Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement non-blocking synchronous evaluation (snapshot API) #81

Merged
merged 9 commits into from
Apr 29, 2024
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
Expand All @@ -23,9 +23,9 @@ public BackdoorController(IConfigCatClient configCatClient)
// This endpoint can be called by Configcat Webhooks https://configcat.com/docs/advanced/notifications-webhooks
[HttpGet]
[Route("configcatchanged")]
public IActionResult ConfigCatChanged()
public async Task<IActionResult> ConfigCatChanged()
{
this.configCatClient.ForceRefresh();
await this.configCatClient.ForceRefreshAsync();

return Ok("configCatClient.ForceRefresh() invoked");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using ConfigCat.Client;
using Microsoft.AspNetCore.Mvc;
using WebApplication.Models;
Expand All @@ -15,9 +15,9 @@ public HomeController(IConfigCatClient configCatClient)
this.configCatClient = configCatClient;
}

public IActionResult Index()
public async Task<IActionResult> Index()
{
ViewData["Message1"] = this.configCatClient.GetValue("isAwesomeFeatureEnabled", false);
ViewData["Message1"] = await this.configCatClient.GetValueAsync("isAwesomeFeatureEnabled", false);

var userObject = new User("<Some UserID>")
{
Expand All @@ -30,7 +30,7 @@ public IActionResult Index()
}
};

ViewData["Message2"] = this.configCatClient.GetValue("isPOCFeatureEnabled", false, userObject);
ViewData["Message2"] = await this.configCatClient.GetValueAsync("isPOCFeatureEnabled", false, userObject);

return View();
}
Expand Down
4 changes: 2 additions & 2 deletions samples/ConsoleApp/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using ConfigCat.Client;

// Creating the ConfigCat client instance using the SDK Key
Expand All @@ -21,5 +21,5 @@
};

// Accessing feature flag or setting value
var value = client.GetValue("isPOCFeatureEnabled", false, user);
var value = await client.GetValueAsync("isPOCFeatureEnabled", false, user);
Console.WriteLine($"isPOCFeatureEnabled: {value}");
5 changes: 3 additions & 2 deletions samples/FileLoggerSample.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Globalization;
using System.IO;
using System.Threading.Tasks;
using ConfigCat.Client;

namespace SampleApplication
Expand Down Expand Up @@ -56,7 +57,7 @@ public void Log(LogLevel level, LogEventId eventId, ref FormattableLogMessage me
}
}

static void Main(string[] args)
static async Task Main(string[] args)
{
var filePath = Path.Combine(Environment.CurrentDirectory, "configcat.log");
var logLevel = LogLevel.Warning; // Log only WARNING and higher entries (warnings and errors).
Expand All @@ -67,7 +68,7 @@ static void Main(string[] args)
options.PollingMode = PollingModes.AutoPoll(pollInterval: TimeSpan.FromSeconds(5));
});

var feature = client.GetValue("keyNotExists", "N/A");
var feature = await client.GetValueAsync("keyNotExists", "N/A");

Console.ReadKey();
}
Expand Down
41 changes: 5 additions & 36 deletions src/ConfigCat.Client.Tests/ConfigCacheTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Threading;
using System.Threading.Tasks;
using ConfigCat.Client.Cache;
using ConfigCat.Client.Tests.Fakes;
using ConfigCat.Client.Tests.Helpers;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
Expand Down Expand Up @@ -70,20 +71,20 @@ public async Task ConfigCache_Override_ManualPoll_Works()
});

configCacheMock.Verify(c => c.SetAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
configCacheMock.Verify(c => c.GetAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
configCacheMock.Verify(c => c.GetAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Once);

var actual = await client.GetValueAsync("stringDefaultCat", "N/A");

Assert.AreEqual("N/A", actual);
configCacheMock.Verify(c => c.SetAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
configCacheMock.Verify(c => c.GetAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Once);
configCacheMock.Verify(c => c.GetAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Exactly(2));

await client.ForceRefreshAsync();

actual = await client.GetValueAsync("stringDefaultCat", "N/A");
Assert.AreEqual("Cat", actual);
configCacheMock.Verify(c => c.SetAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Once);
configCacheMock.Verify(c => c.GetAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Exactly(3));
configCacheMock.Verify(c => c.GetAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Exactly(4));
}

[TestMethod]
Expand Down Expand Up @@ -242,40 +243,8 @@ public void CacheKeyGeneration_ShouldBePlatformIndependent(string sdkKey, string
public void CachePayloadSerialization_ShouldBePlatformIndependent(string configJson, string timeStamp, string httpETag, string expectedPayload)
{
var timeStampDateTime = DateTimeOffset.ParseExact(timeStamp, "o", CultureInfo.InvariantCulture).UtcDateTime;
var pc = new ProjectConfig(configJson, configJson.Deserialize<Config>(), timeStampDateTime, httpETag);
var pc = new ProjectConfig(configJson, Config.Deserialize(configJson.AsMemory()), timeStampDateTime, httpETag);

Assert.AreEqual(expectedPayload, ProjectConfig.Serialize(pc));
}

private sealed class FakeExternalCache : IConfigCatCache
{
public volatile string? CachedValue = null;

public string? Get(string key) => this.CachedValue;

public Task<string?> GetAsync(string key, CancellationToken cancellationToken = default) => Task.FromResult(Get(key));

public void Set(string key, string value) => this.CachedValue = value;

public Task SetAsync(string key, string value, CancellationToken cancellationToken = default)
{
Set(key, value);
return Task.FromResult(0);
}
}

private sealed class FaultyFakeExternalCache : IConfigCatCache
{
public string? Get(string key) => throw new ApplicationException("Operation failed :(");

public Task<string?> GetAsync(string key, CancellationToken cancellationToken = default) => Task.FromResult(Get(key));

public void Set(string key, string value) => throw new ApplicationException("Operation failed :(");

public Task SetAsync(string key, string value, CancellationToken cancellationToken = default)
{
Set(key, value);
return Task.FromResult(0);
}
}
}
1 change: 1 addition & 0 deletions src/ConfigCat.Client.Tests/ConfigCat.Client.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<LangVersion>10.0</LangVersion>
<Nullable>enable</Nullable>
<WarningsAsErrors>nullable</WarningsAsErrors>
<NoWarn>CS0618</NoWarn>
<AssemblyOriginatorKeyFile>..\ConfigCatClient.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>

Expand Down
71 changes: 71 additions & 0 deletions src/ConfigCat.Client.Tests/ConfigCatClientSnapshotTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using ConfigCat.Client.Cache;
using ConfigCat.Client.ConfigService;
using ConfigCat.Client.Configuration;
using ConfigCat.Client.Evaluation;
using ConfigCat.Client.Override;
using ConfigCat.Client.Tests.Fakes;
using ConfigCat.Client.Tests.Helpers;
using ConfigCat.Client.Utils;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;

namespace ConfigCat.Client.Tests;

[TestClass]
public class ConfigCatClientSnapshotTests
{
[TestMethod]
public void DefaultInstanceDoesNotThrow()
{
const string key = "key";
const string defaultValue = "";

var snapshot = default(ConfigCatClientSnapshot);

Assert.AreEqual(ClientCacheState.NoFlagData, snapshot.CacheState);
Assert.IsNull(snapshot.FetchedConfig);
CollectionAssert.AreEqual(ArrayUtils.EmptyArray<string>(), snapshot.GetAllKeys().ToArray());
Assert.AreEqual("", snapshot.GetValue(key, defaultValue));
var evaluationDetails = snapshot.GetValueDetails(key, defaultValue);
Assert.IsNotNull(evaluationDetails);
Assert.AreEqual(key, evaluationDetails.Key);
Assert.AreEqual(defaultValue, evaluationDetails.Value);
Assert.IsTrue(evaluationDetails.IsDefaultValue);
Assert.IsNotNull(evaluationDetails.ErrorMessage);
}

[TestMethod]
public void CanMockSnapshot()
{
const ClientCacheState cacheState = ClientCacheState.HasUpToDateFlagData;
var fetchedConfig = new Config();
var keys = new[] { "key1", "key2" };
var evaluationDetails = new EvaluationDetails<string>("key1", "value");

var mock = new Mock<IConfigCatClientSnapshot>();
mock.SetupGet(m => m.CacheState).Returns(cacheState);
mock.SetupGet(m => m.FetchedConfig).Returns(fetchedConfig);
mock.Setup(m => m.GetAllKeys()).Returns(keys);
mock.Setup(m => m.GetValue(evaluationDetails.Key, It.IsAny<string>(), It.IsAny<User?>())).Returns(evaluationDetails.Value);
mock.Setup(m => m.GetValueDetails(evaluationDetails.Key, It.IsAny<string>(), It.IsAny<User?>())).Returns(evaluationDetails);

var snapshot = new ConfigCatClientSnapshot(mock.Object);

Assert.AreEqual(cacheState, snapshot.CacheState);
Assert.AreEqual(fetchedConfig, snapshot.FetchedConfig);
CollectionAssert.AreEqual(keys, snapshot.GetAllKeys().ToArray());
Assert.AreEqual(evaluationDetails.Value, snapshot.GetValue(evaluationDetails.Key, ""));
Assert.AreSame(evaluationDetails, snapshot.GetValueDetails(evaluationDetails.Key, ""));
}
}
Loading
Loading