diff --git a/CHANGELOG.md b/CHANGELOG.md index fc13ac10..ea427486 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,205 +1 @@ -### 7.2.0 - -- Introduced a new local dictionary override factory method: - ```csharp - FlagOverrides LocalDictionary(IDictionary dictionary, bool watchChanges, OverrideBehaviour overrideBehaviour) - ``` - Where the `watchChanges` parameter indicates whether the SDK should rebuild the overrides upon each read to keep track of the source dictionary's changes. -- Fix config fetcher-related error logging to include exception in the log if any. - -### 7.1.0 - -- Add new evaluation methods `GetAllValueDetails`/`GetAllValueDetailsAsync`. -- Fix logging in `ConfigServiceBase.SetOnline`. -- Correct behavior of `GetAllXXX` methods so `FlagEvaluated` event is also raised in case of error. -- Correct reporting of "Config JSON is not present" errors and log them with error level also in the case of `GetAllXXX` methods. -- Change implementation of `HttpConfigFetcher.FetchAsync` to execute only one fetch operation at a time. -- Make `ProjectConfig` equality comparison consistent with other SDKs (treats `ProjectConfig` instances with the same ETag equal regardless of actual content). -- Make HTTP response handling consistent with other SDKs. -- Make `HttpConfigFetcher`-related error message consistent with other SDKs. - -### 7.0.0 -- Deprecate `ConfigCatClient` constructors in favor of the new static factory method `Get`, - which provides single client instances per SDK key. -- Add convenience method `DisposeAll` for disposing all open clients at once. -- Implement default user feature. -- Implement offline mode feature. -- Improve LazyLoad and AutoPoll refresh logic by taking the cache timestamp into account - to fetch the config only if cached config is unavailable or stale. -- Add new evaluation methods `GetValueDetail`/`GetValueDetailsAsync`, - which provide more detailed information about the evaluation result. -- Add hooks (events), which provide notifications of the client's actions. -- Additional minor code quality and performance improvements. -- Update samples to .NET 6. - -### 6.5.3 -- Use logger wrapper everywhere internally. #39 -- Improved evaluation logging. #38 - -### 6.5.2 -- Consolidate percentage rule evaluation logs. - -### 6.5.1 -- Add net461 to the target frameworks list to force the usage of `System.Text.Json` rather than `Newtonsoft.Json`. - -### 6.5.0 -- Replace `FileSystemWatcher` with file polling in local file override data source. - -### 6.4.12 -- Fix various local file override data source issues. - -### 6.4.9 -- Move the PollingMode option to public scope. - -### 6.4.8 -- Readd `System.Text.RegularExpressions` version `4.3.1` due to SNYK security report. - -### 6.4.7 -- Remove unused `System.Text.RegularExpressions` dependency. - -### 6.4.6 -- Fix the wait time calculation in auto-polling mode. - -### 6.4.3 -- Fix README links displayed on the NuGet package page. - -### 6.4.0 -- **Introduced a new configuration API replacing the builder pattern**: - - ```cs - ConfigCatClientBuilder - .Initialize(SDKKEY) - .WithLogger(consoleLogger) - .WithAutoPoll() - .WithMaxInitWaitTimeSeconds(5) - .WithPollIntervalSeconds(60) - .Create(); - ``` - - Will look like this: - ```cs - new ConfigCatClient(options => - { - options.SdkKey = SDKKEY; - options.PollingMode = PollingModes.AutoPoll(TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(5)); - options.Logger = consoleLogger; - }); - ``` - - The old API is still available it's just marked with the `[Obsolete]` attribute. - -- **`GetAllValues()` and `GetAllValuesAsync()`**: - These methods are now evaluating all feature flags and settings into an `IDictionary`. - -- **FlagOverrides**: - It's now possible to feed the SDK with local feature flag and setting values. - - **Dictionary** - ```cs - var dict = new Dictionary - { - {"enabledFeature", true}, - {"intSetting", 5}, - }; - - using var client = new ConfigCatClient(options => - { - options.SdkKey = "localhost"; - options.FlagOverrides = FlagOverrides.LocalDictionary(dict, - OverrideBehaviour.LocalOnly); - }); - ``` - - **File** - ```cs - using var client = new ConfigCatClient(options => - { - options.SdkKey = "localhost"; - options.FlagOverrides = FlagOverrides.LocalFile("path/to/file", - autoReload: false, - overrideBehaviour: OverrideBehaviour.LocalOnly); - }); - ``` - Three behaviours available: `LocalOnly`, `LocalOverRemote`, and `RemoteOverLocal`. - With `LocalOnly` the SDK switches into a complete offline state, and only the override values are served. - `LocalOverRemote` and `RemoteOverLocal` merge the local and remote feature flag values respecting one or another in case of key duplications. - -- **Changes in JSON handling**: - In respect of [#30](https://github.com/configcat/.net-sdk/issues/30) `System.Text.Json` is favored over `Newtonsoft.Json` in frameworks newer than `net45`. `System.Text.Json` is not available for `net45` so that target remains using `Newtonsoft.Json`. - -- **`net5.0` and `net6.0` target frameworks**. - -- **HttpTimeout configuration option**. - -- **Solution for** [#26](https://github.com/configcat/.net-sdk/issues/26). - To prevent possible deadlocks the following changes were applied: - - Created a synchronous extension for the existing fully async `IConfigCache`. In the future we will replace that interface with the new one (`IConfigCatCache`) that has now the sync API and inherits the async API from `IConfigCache`. `IConfigCache` was marked with `[Obsolete]` to maintain backward compatibility. `InMemoryConfigCache` now implements both sync and async APIs through `IConfigCatCache`. - - Extended the config services (`AutoPoll`, `LazyLoad`, `ManualPoll`) with synchronous branches that are using the new cache's sync / async methods in respect of sync and async customer calls. - - Extended the `HttpConfigFetcher` with a synchronous `Fetch` that uses the `HttpClient`'s `Send()` method where it's available (`net5.0` and above). Below `net5.0` the synchronous path falls back to a functionality that queues the HTTP request to a thread pool thread and waits for its completion. This solution prevents deadlocks however, it puts more load on the thread pool. - -- **CI Changes**: - - Introduced new [GitHub actions for Linux and macOS builds](https://github.com/configcat/.net-sdk/actions/workflows/linux-macOS-CI.yml). - - The [sonarcloud analysis](https://github.com/configcat/.net-sdk/actions/workflows/sonar-analysis.yml) is moved to a separate Action from the appveyor task. - - Removed codecov completely, it will be replaced by the coverage data from sonarcloud. - -### 6.2.1 -- Reducing the number of json deserializations between `GetValue` calls. -### 6.1.20 -- Bugfix: The SDK's json serialization behavior is not depending on the `JsonConvert.DefaultSettings` anymore. -### 6.1.0 -- Bugfix ([#17](https://github.com/configcat/.net-sdk/issues/17)) -### 6.0.0 -- Addressing global data handling and processing trends via Data Governance feature. Customers can control the geographic location where their config JSONs get published to. [See the docs](https://configcat.com/docs/advanced/data-governance/). -We are introducing a new DataGovernance initialization parameter. Set this parameter to be in sync with the Data Governance preference on the [Dashboard](https://app.configcat.com/organization/data-governance). - -#### Breaking changes: -### 7.1.0 -- [API] Remove `BeforeClientDispose` hook -### 7.0.0 -- [API] Add new methods to the `IConfigCatClient` interface. -- [API] Change `ProjectConfig` to reference type with value equality (record). -- [Behavior] Slightly changes the behavior of ProjectConfig.TimeStamp (only updated when communication with the CDN servers succeeds, regardless of the returned status code.) -### 6.0.0 -- Custom cache implementations should implement the new cache interface using key parameter in the get/set methods. -### 5.3.0 -- VariationID, bugfix ([#11](https://github.com/configcat/.net-sdk/issues/11)) -### 5.2.0 -- Bugfix (config fetch, caching) -### 5.1.0 -- Remove semver nuget packages -### 5.0.0 -- Breaking change: Renamed `API Key` to `SDK Key`. -### 4.0.0 -- Supporting sensitive text comparators. -### 3.2.0 -- Minor fix in info level logging -### 3.1.0 -- Added new semantic version tests -### 3.0.0 -- Support new types (number, semver), detailed log entries, compressed http communication -### 2.5.0 -- Support custom HttpClientHandler -### 2.4.0 -- Add GetAllKeys() function -### 2.3.0 -- BaseUrl override oppurtunity -- IConfigCache override oppurtunity -### 2.3.0 -- BaseUrl override oppurtunity -- IConfigCache override oppurtunity -### 2.2.1 -- Bugfix (logger level) -### 2.2.0 -- Namespace unification -### 2.1.0 -- Rollout handling v2 -### 2.0.1 -- Bugfix -### 2.0.0 -- Implement rollout feature -### 1.0.7 -- Implement LazyLoad, AutoPoll, ManualPoll feature -### 1.0.6 -- Finalize logging -### 1.0.5 -- Implement tracing, add clear cache ability to client -### 1.0.4 -- Initial release +Please check the [Github Releases](https://github.com/configcat/.net-sdk/releases) page for the changelog of the ConfigCat SDK for .NET. \ No newline at end of file diff --git a/DEPLOY.md b/DEPLOY.md index 4f9fa9f9..8696a5d2 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -1,13 +1,12 @@ # Steps to Deploy 1. Run tests 2. Set version in `appveyor.yml` (e.g: from `build_version: 6.5.0` to `build_version: 6.5.1`) -3. Update release notes in CHANGELOG.md -4. Open a PR -5. When the PR is merged to master, start a deploy to NuGet.org +3. Open a PR +4. When the PR is merged to master, start a deploy to NuGet.org Create a new deployment on https://ci.appveyor.com/project/configcat/net-sdk/deployments -6. Make sure new package is available via Nuget.org: https://www.nuget.org/packages/ConfigCat.Client -7. Update and test sample apps with the new SDK version. +5. Make sure new package is available via Nuget.org: https://www.nuget.org/packages/ConfigCat.Client +6. Update and test sample apps with the new SDK version. *Usually it takes a few minutes to propagate.* -8. Add release notes: https://github.com/configcat/.net-sdk/releases \ No newline at end of file +7. Add release notes: https://github.com/configcat/.net-sdk/releases \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml index ab9b13d7..a84073ab 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,5 +1,5 @@ environment: - build_version: 7.2.0 + build_version: 8.0.0 version: $(build_version)-{build} image: Visual Studio 2022 configuration: Release @@ -11,10 +11,10 @@ dotnet_csproj: patch: true file: '**\*.csproj' version: $(build_version) - package_version: $(build_version) + package_version: $(build_version)-preview1 assembly_version: $(build_version) file_version: $(build_version) - informational_version: $(build_version) + informational_version: $(build_version)-preview1 install: - cmd: dotnet tool install -g InheritDocTool build_script: @@ -24,9 +24,10 @@ build_script: after_build: - cmd: echo __PACK__ - inheritdoc -o -- dotnet pack -c %configuration% /p:IncludeSymbols=true /p:PackageOutputPath=..\..\artifacts /p:SymbolPackageFormat=snupkg src\ConfigCatClient\ConfigCatClient.csproj +- dotnet pack -c %configuration% /p:PackageOutputPath=..\..\artifacts /p:ContinuousIntegrationBuild=true src\ConfigCatClient\ConfigCatClient.csproj test_script: - dotnet test src\ConfigCat.Client.Tests\ConfigCat.Client.Tests.csproj -f net45 -c %configuration% --no-build +- dotnet test src\ConfigCat.Client.Tests\ConfigCat.Client.Tests.csproj -f net461 -c %configuration% --no-build - dotnet test src\ConfigCat.Client.Tests\ConfigCat.Client.Tests.csproj -f netcoreapp3.1 -c %configuration% --no-build - dotnet test src\ConfigCat.Client.Tests\ConfigCat.Client.Tests.csproj -f net5.0 -c %configuration% --no-build - dotnet test src\ConfigCat.Client.Tests\ConfigCat.Client.Tests.csproj -f net6.0 -c %configuration% --no-build diff --git a/samples/ASP.NETCore/WebApplication/Adapters/ConfigCatToMSLoggerAdapter.cs b/samples/ASP.NETCore/WebApplication/Adapters/ConfigCatToMSLoggerAdapter.cs index a26505c3..9a9a1504 100644 --- a/samples/ASP.NETCore/WebApplication/Adapters/ConfigCatToMSLoggerAdapter.cs +++ b/samples/ASP.NETCore/WebApplication/Adapters/ConfigCatToMSLoggerAdapter.cs @@ -15,7 +15,11 @@ public ConfigCatToMSLoggerAdapter(ILogger logg } // Allow all log levels here and let MS logger do log level filtering (see appsettings.json) - public ConfigCat.Client.LogLevel LogLevel { get; set; } = ConfigCat.Client.LogLevel.Debug; + public ConfigCat.Client.LogLevel LogLevel + { + get => ConfigCat.Client.LogLevel.Debug; + set { } + } public void Log(ConfigCat.Client.LogLevel level, ConfigCat.Client.LogEventId eventId, ref ConfigCat.Client.FormattableLogMessage message, Exception? exception = null) { diff --git a/samples/FileLoggerSample.cs b/samples/FileLoggerSample.cs index 753c5954..21ff4878 100644 --- a/samples/FileLoggerSample.cs +++ b/samples/FileLoggerSample.cs @@ -9,10 +9,17 @@ class Program { class MyFileLogger : IConfigCatLogger { - private readonly string filePath; private static readonly object SyncObj = new object(); - public LogLevel LogLevel { get; set; } + private readonly string filePath; + + private volatile LogLevel logLevel; + + public LogLevel LogLevel + { + get => this.logLevel; + set => this.logLevel = value; + } public MyFileLogger(string filePath, LogLevel logLevel) { diff --git a/src/ConfigCat.Client.Tests/BasicConfigCatClientIntegrationTests.cs b/src/ConfigCat.Client.Tests/BasicConfigCatClientIntegrationTests.cs index 2a1cf810..b856926b 100644 --- a/src/ConfigCat.Client.Tests/BasicConfigCatClientIntegrationTests.cs +++ b/src/ConfigCat.Client.Tests/BasicConfigCatClientIntegrationTests.cs @@ -1,6 +1,4 @@ using System; -using System.CodeDom; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Net.Http; @@ -8,7 +6,6 @@ using System.Threading.Tasks; using ConfigCat.Client.Configuration; using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; namespace ConfigCat.Client.Tests; diff --git a/src/ConfigCat.Client.Tests/BasicConfigEvaluatorTests.cs b/src/ConfigCat.Client.Tests/BasicConfigEvaluatorTests.cs index 011e0f35..b1c94fe6 100644 --- a/src/ConfigCat.Client.Tests/BasicConfigEvaluatorTests.cs +++ b/src/ConfigCat.Client.Tests/BasicConfigEvaluatorTests.cs @@ -1,11 +1,8 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Reflection; -using System.Runtime.CompilerServices; using ConfigCat.Client.Evaluation; using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; namespace ConfigCat.Client.Tests; diff --git a/src/ConfigCat.Client.Tests/ConfigCacheTests.cs b/src/ConfigCat.Client.Tests/ConfigCacheTests.cs index 8386b571..5631c51a 100644 --- a/src/ConfigCat.Client.Tests/ConfigCacheTests.cs +++ b/src/ConfigCat.Client.Tests/ConfigCacheTests.cs @@ -157,14 +157,14 @@ public async Task ConfigCache_Works(bool isExternal, bool isAsync) Task ReadCacheAsync() { - return isAsync ? configCache.GetAsync(cacheKey) : Task.FromResult(configCache.Get(cacheKey)); + return isAsync ? configCache.GetAsync(cacheKey).AsTask() : Task.FromResult(configCache.Get(cacheKey)); } Task WriteCacheAsync(ProjectConfig config) { if (isAsync) { - return configCache.SetAsync(cacheKey, config); + return configCache.SetAsync(cacheKey, config).AsTask(); } else { diff --git a/src/ConfigCat.Client.Tests/ConfigCatClientCacheTests.cs b/src/ConfigCat.Client.Tests/ConfigCatClientCacheTests.cs index 960d57e2..72e052ae 100644 --- a/src/ConfigCat.Client.Tests/ConfigCatClientCacheTests.cs +++ b/src/ConfigCat.Client.Tests/ConfigCatClientCacheTests.cs @@ -1,7 +1,5 @@ using System; -using System.Linq; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using ConfigCat.Client.Configuration; using Microsoft.VisualStudio.TestTools.UnitTesting; diff --git a/src/ConfigCat.Client.Tests/ConfigCatClientTests.cs b/src/ConfigCat.Client.Tests/ConfigCatClientTests.cs index ee235073..ecef4e4e 100644 --- a/src/ConfigCat.Client.Tests/ConfigCatClientTests.cs +++ b/src/ConfigCat.Client.Tests/ConfigCatClientTests.cs @@ -1257,6 +1257,9 @@ public void DisposeAll_CachedInstancesRemoved() instanceCount1 = ConfigCatClient.Instances.GetAliveCount(); + GC.KeepAlive(client1); + GC.KeepAlive(client2); + ConfigCatClient.DisposeAll(); var instanceCount2 = ConfigCatClient.Instances.GetAliveCount(); @@ -1284,6 +1287,9 @@ static void CreateClients(out int instanceCount) var client2 = ConfigCatClient.Get("test2", options => options.PollingMode = PollingModes.ManualPoll); instanceCount = ConfigCatClient.Instances.GetAliveCount(); + + GC.KeepAlive(client1); + GC.KeepAlive(client2); } // Act @@ -1801,14 +1807,14 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } - public Task GetConfigAsync(CancellationToken cancellationToken = default) + public ValueTask GetConfigAsync(CancellationToken cancellationToken = default) { - return Task.FromResult(ProjectConfig.Empty); + return new ValueTask(ProjectConfig.Empty); } - public override Task RefreshConfigAsync(CancellationToken cancellationToken = default) + public override ValueTask RefreshConfigAsync(CancellationToken cancellationToken = default) { - return Task.FromResult(RefreshConfig()); + return new ValueTask(RefreshConfig()); } public ProjectConfig GetConfig() diff --git a/src/ConfigCat.Client.Tests/ConfigServiceTests.cs b/src/ConfigCat.Client.Tests/ConfigServiceTests.cs index 44976dc2..0c7c78a8 100644 --- a/src/ConfigCat.Client.Tests/ConfigServiceTests.cs +++ b/src/ConfigCat.Client.Tests/ConfigServiceTests.cs @@ -1,11 +1,9 @@ using System; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using ConfigCat.Client.Cache; using ConfigCat.Client.ConfigService; -using ConfigCat.Client.Evaluation; using ConfigCat.Client.Tests.Helpers; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -42,7 +40,7 @@ public async Task LazyLoadConfigService_GetConfigAsync_ReturnsExpiredContent_Sho this.cacheMock .Setup(m => m.SetAsync(It.IsAny(), this.fetchedPc, It.IsAny())) - .Returns(Task.FromResult(0)) + .Returns(default(ValueTask)) .Verifiable(); this.fetcherMock @@ -118,7 +116,7 @@ public async Task LazyLoadConfigService_RefreshConfigAsync_ShouldNotInvokeCacheG this.cacheMock .Setup(m => m.SetAsync(It.IsAny(), this.fetchedPc, It.IsAny())) - .Returns(Task.FromResult(0)) + .Returns(default(ValueTask)) .Callback(() => Assert.AreEqual(3, callOrder)) .Verifiable(); @@ -158,7 +156,7 @@ public async Task LazyLoadConfigService_RefreshConfigAsync_ConfigChanged_ShouldR this.cacheMock .Setup(m => m.SetAsync(It.IsAny(), this.fetchedPc, It.IsAny())) - .Returns(Task.FromResult(0)); + .Returns(default(ValueTask)); using var service = new LazyLoadConfigService( this.fetcherMock.Object, @@ -196,7 +194,7 @@ public async Task AutoPollConfigService_GetConfigAsync_WithoutTimerWithCachedCon this.cacheMock .Setup(m => m.SetAsync(It.IsAny(), this.fetchedPc, It.IsAny())) .Callback(() => localPc = this.fetchedPc) - .Returns(Task.FromResult(0)); + .Returns(default(ValueTask)); var config = PollingModes.AutoPoll(TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(60)); using var service = new AutoPollConfigService(config, @@ -234,7 +232,7 @@ public async Task AutoPollConfigService_GetConfigAsync_WithTimer_ShouldInvokeFet this.cacheMock .Setup(m => m.SetAsync(It.IsAny(), this.fetchedPc, It.IsAny())) .Callback(() => wd.Set()) - .Returns(Task.FromResult(0)); + .Returns(default(ValueTask)); var config = PollingModes.AutoPoll(TimeSpan.FromSeconds(50), TimeSpan.FromSeconds(0)); using var service = new AutoPollConfigService(config, @@ -271,7 +269,7 @@ public async Task AutoPollConfigService_RefreshConfigAsync_ShouldOnceInvokeCache this.cacheMock .Setup(m => m.SetAsync(It.IsAny(), this.fetchedPc, It.IsAny())) - .Returns(Task.FromResult(0)); + .Returns(default(ValueTask)); var config = PollingModes.AutoPoll(TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(0)); using var service = new AutoPollConfigService(config, @@ -301,7 +299,7 @@ public async Task AutoPollConfigService_Dispose_ShouldStopTimer() this.cacheMock .Setup(m => m.GetAsync(It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(this.cachedPc)); + .ReturnsAsync(this.cachedPc); this.fetcherMock .Setup(m => m.FetchAsync(this.cachedPc, It.IsAny())) @@ -421,7 +419,7 @@ public async Task ManualPollConfigService_RefreshConfigAsync_ShouldInvokeCacheGe this.cacheMock .Setup(m => m.SetAsync(It.IsAny(), this.fetchedPc, It.IsAny())) .Callback(() => Assert.AreEqual(3, callOrder++)) - .Returns(Task.FromResult(0)); + .Returns(default(ValueTask)); using var service = new ManualPollConfigService( this.fetcherMock.Object, @@ -461,7 +459,7 @@ public async Task ManualPollConfigService_RefreshConfigAsync_ConfigChanged_Shoul this.cacheMock .Setup(m => m.SetAsync(It.IsAny(), this.fetchedPc, It.IsAny())) - .Returns(Task.FromResult(0)); + .Returns(default(ValueTask)); using var service = new ManualPollConfigService( this.fetcherMock.Object, diff --git a/src/ConfigCat.Client.Tests/DataGovernanceTests.cs b/src/ConfigCat.Client.Tests/DataGovernanceTests.cs index 740ce7a0..4dbbd62f 100644 --- a/src/ConfigCat.Client.Tests/DataGovernanceTests.cs +++ b/src/ConfigCat.Client.Tests/DataGovernanceTests.cs @@ -5,7 +5,6 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; -using ConfigCat.Client; using ConfigCat.Client.Configuration; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; diff --git a/src/ConfigCat.Client.Tests/HttpConfigFetcherTests.cs b/src/ConfigCat.Client.Tests/HttpConfigFetcherTests.cs index ab689784..d303947b 100644 --- a/src/ConfigCat.Client.Tests/HttpConfigFetcherTests.cs +++ b/src/ConfigCat.Client.Tests/HttpConfigFetcherTests.cs @@ -3,7 +3,6 @@ using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; -using ConfigCat.Client.Evaluation; using ConfigCat.Client.Tests.Helpers; using Microsoft.VisualStudio.TestTools.UnitTesting; diff --git a/src/ConfigCat.Client.Tests/OverrideTests.cs b/src/ConfigCat.Client.Tests/OverrideTests.cs index 8dc5d154..7f758dac 100644 --- a/src/ConfigCat.Client.Tests/OverrideTests.cs +++ b/src/ConfigCat.Client.Tests/OverrideTests.cs @@ -466,6 +466,111 @@ public async Task LocalFile_Watcher_Reload_Sync() File.Delete(SampleFileToCreate); } + [DataRow(true, false, true)] + [DataRow(true, "", "")] + [DataRow(true, 0, 0)] + [DataRow(true, 0.0, 0.0)] + [DataRow("text", false, false)] + [DataRow("text", "", "text")] + [DataRow("text", 0, 0)] + [DataRow("text", 0.0, 0.0)] + [DataRow(42, false, false)] + [DataRow(42, "", "")] + [DataRow(42, 0, 42)] + [DataRow(42, 0.0, 0.0)] + [DataRow(42.0, false, false)] + [DataRow(42.0, "", "")] + [DataRow(42.0, 0, 0)] + [DataRow(42.0, 0.0, 42.0)] + [DataRow(3.14, false, false)] + [DataRow(3.14, "", "")] + [DataRow(3.14, 0, 0)] + [DataRow(3.14, 0.0, 3.14)] + [DataRow(null, false, false)] + [DataRow(new object[0], false, false)] + [DataRow(typeof(object), false, false)] + [DataRow(typeof(DateTime), false, false)] + [DataTestMethod] + public void OverrideValueTypeMismatchShouldBeHandledCorrectly_Dictionary(object overrideValue, object defaultValue, object expectedEvaluatedValue) + { + const string key = "flag"; + + var dictionary = new Dictionary + { + [key] = overrideValue is not Type valueType ? overrideValue : Activator.CreateInstance(valueType)! + }; + + using var client = ConfigCatClient.Get("localhost", options => + { + options.PollingMode = PollingModes.ManualPoll; + options.FlagOverrides = FlagOverrides.LocalDictionary(dictionary, OverrideBehaviour.LocalOnly); + }); + + var method = typeof(IConfigCatClient).GetMethod(nameof(IConfigCatClient.GetValue))! + .GetGenericMethodDefinition() + .MakeGenericMethod(defaultValue.GetType()); + + var actualEvaluatedValue = method.Invoke(client, new[] { key, defaultValue, null }); + + Assert.AreEqual(expectedEvaluatedValue, actualEvaluatedValue); + } + + [DataRow("true", false, true)] + [DataRow("true", "", "")] + [DataRow("true", 0, 0)] + [DataRow("true", 0.0, 0.0)] + [DataRow("\"text\"", false, false)] + [DataRow("\"text\"", "", "text")] + [DataRow("\"text\"", 0, 0)] + [DataRow("\"text\"", 0.0, 0.0)] + [DataRow("42", false, false)] + [DataRow("42", "", "")] + [DataRow("42", 0, 42)] + [DataRow("42", 0.0, 0.0)] + [DataRow("42.0", false, false)] + [DataRow("42.0", "", "")] + [DataRow("42.0", 0, 0)] + [DataRow("42.0", 0.0, 42.0)] + [DataRow("3.14", false, false)] + [DataRow("3.14", "", "")] + [DataRow("3.14", 0, 0)] + [DataRow("3.14", 0.0, 3.14)] + [DataRow("null", false, false)] + [DataRow("[]", false, false)] + [DataRow("{}", false, false)] + [DataTestMethod] + public void OverrideValueTypeMismatchShouldBeHandledCorrectly_SimplifiedConfig(string overrideValueJson, object defaultValue, object expectedEvaluatedValue) + { + const string key = "flag"; + + var filePath = Path.GetTempFileName(); + File.WriteAllText(filePath, $"{{ \"flags\": {{ \"{key}\": {overrideValueJson} }} }}"); + + try + { + using var client = ConfigCatClient.Get("localhost", options => + { + options.PollingMode = PollingModes.ManualPoll; + options.FlagOverrides = FlagOverrides.LocalFile(filePath, autoReload: false, OverrideBehaviour.LocalOnly); + }); + + var method = typeof(IConfigCatClient).GetMethod(nameof(IConfigCatClient.GetValue))! + .GetGenericMethodDefinition() + .MakeGenericMethod(defaultValue.GetType()); + + var actualEvaluatedValue = method.Invoke(client, new[] { key, defaultValue, null }); + + Assert.AreEqual(expectedEvaluatedValue, actualEvaluatedValue); + } + finally + { + if (File.Exists(filePath)) + { + File.Delete(filePath); + } + } + } + private static string GetJsonContent(string value) { return $"{{ \"f\": {{ \"fakeKey\": {{ \"v\": \"{value}\", \"p\": [] ,\"r\": [] }} }} }}"; diff --git a/src/ConfigCatClient/Cache/ConfigCache.cs b/src/ConfigCatClient/Cache/ConfigCache.cs index d43a33de..f46a0ed4 100644 --- a/src/ConfigCatClient/Cache/ConfigCache.cs +++ b/src/ConfigCatClient/Cache/ConfigCache.cs @@ -10,7 +10,7 @@ protected ConfigCache() { } public abstract ProjectConfig LocalCachedConfig { get; } public abstract void Set(string key, ProjectConfig config); - public abstract Task SetAsync(string key, ProjectConfig config, CancellationToken cancellationToken = default); + public abstract ValueTask SetAsync(string key, ProjectConfig config, CancellationToken cancellationToken = default); public abstract ProjectConfig Get(string key); - public abstract Task GetAsync(string key, CancellationToken cancellationToken = default); + public abstract ValueTask GetAsync(string key, CancellationToken cancellationToken = default); } diff --git a/src/ConfigCatClient/Cache/ExternalConfigCache.cs b/src/ConfigCatClient/Cache/ExternalConfigCache.cs index 309f2677..1b8dd3bc 100644 --- a/src/ConfigCatClient/Cache/ExternalConfigCache.cs +++ b/src/ConfigCatClient/Cache/ExternalConfigCache.cs @@ -42,7 +42,7 @@ public override ProjectConfig Get(string key) } } - public override async Task GetAsync(string key, CancellationToken cancellationToken = default) + public override async ValueTask GetAsync(string key, CancellationToken cancellationToken = default) { try { @@ -86,7 +86,7 @@ public override void Set(string key, ProjectConfig config) } } - public override async Task SetAsync(string key, ProjectConfig config, CancellationToken cancellationToken = default) + public override async ValueTask SetAsync(string key, ProjectConfig config, CancellationToken cancellationToken = default) { try { diff --git a/src/ConfigCatClient/Cache/IConfigCatCache.cs b/src/ConfigCatClient/Cache/IConfigCatCache.cs index d6eabe36..1a406cff 100644 --- a/src/ConfigCatClient/Cache/IConfigCatCache.cs +++ b/src/ConfigCatClient/Cache/IConfigCatCache.cs @@ -4,37 +4,38 @@ namespace ConfigCat.Client; /// -/// Defines the interface used by the ConfigCat SDK to store and retrieve downloaded config JSON data. +/// Defines the interface used by the ConfigCat SDK to store and retrieve downloaded config data. /// public interface IConfigCatCache { /// - /// Stores a value into the cache. + /// Stores a data item into the cache synchronously. /// - /// A string identifying the value. - /// The value to cache. + /// A string identifying the data item. + /// The data item to cache. void Set(string key, string value); /// - /// Stores a value into the cache. + /// Stores a data item into the cache asynchronously. /// - /// A string identifying the value. - /// The value to cache. + /// A string identifying the data item. + /// The data item to cache. /// A to observe while waiting for the task to complete. + /// A task that represents the asynchronous operation. Task SetAsync(string key, string value, CancellationToken cancellationToken = default); /// - /// Retrieves a value from the cache. + /// Retrieves a data item from the cache synchronously. /// - /// A string identifying the value. - /// The cached value or if there is none. + /// A string identifying the data item. + /// The cached data item or if there is none. string? Get(string key); /// - /// Retrieves a value from the cache. + /// Retrieves a data item from the cache asynchronously. /// - /// A string identifying the value. + /// A string identifying the data item. /// A to observe while waiting for the task to complete. - /// The cached value or if there is none. + /// A task that represents the asynchronous operation. The task result contains the cached data item or if there is none. Task GetAsync(string key, CancellationToken cancellationToken = default); } diff --git a/src/ConfigCatClient/Cache/InMemoryConfigCache.cs b/src/ConfigCatClient/Cache/InMemoryConfigCache.cs index 3d8fe8f6..af852fcb 100644 --- a/src/ConfigCatClient/Cache/InMemoryConfigCache.cs +++ b/src/ConfigCatClient/Cache/InMemoryConfigCache.cs @@ -1,4 +1,3 @@ -using System; using System.Threading; using System.Threading.Tasks; @@ -15,14 +14,14 @@ public override ProjectConfig Get(string key) return LocalCachedConfig; } - public override Task GetAsync(string key, CancellationToken cancellationToken = default) + public override ValueTask GetAsync(string key, CancellationToken cancellationToken = default) { if (cancellationToken.IsCancellationRequested) { - return cancellationToken.ToTask(); + return new ValueTask(cancellationToken.ToTask()); } - return Task.FromResult(Get(key)); + return new ValueTask(Get(key)); } public override void Set(string key, ProjectConfig config) @@ -30,18 +29,15 @@ public override void Set(string key, ProjectConfig config) this.cachedConfig = config; } - public override Task SetAsync(string key, ProjectConfig config, CancellationToken cancellationToken = default) + public override ValueTask SetAsync(string key, ProjectConfig config, CancellationToken cancellationToken = default) { if (cancellationToken.IsCancellationRequested) { - return cancellationToken.ToTask(); + return new ValueTask(cancellationToken.ToTask()); } Set(key, config); -#if NET45 - return Task.FromResult(0); -#else - return Task.CompletedTask; -#endif + + return default; } } diff --git a/src/ConfigCatClient/ConfigCatClient.cs b/src/ConfigCatClient/ConfigCatClient.cs index a59f2e1e..e98812ae 100644 --- a/src/ConfigCatClient/ConfigCatClient.cs +++ b/src/ConfigCatClient/ConfigCatClient.cs @@ -14,7 +14,7 @@ namespace ConfigCat.Client; /// -/// Client for ConfigCat platform +/// ConfigCat SDK client. /// public sealed class ConfigCatClient : IConfigCatClient { @@ -41,51 +41,49 @@ public LogLevel LogLevel set => this.logger.LogLevel = value; } - internal ConfigCatClient(string sdkKey, ConfigCatClientOptions configuration) + internal ConfigCatClient(string sdkKey, ConfigCatClientOptions options) { this.sdkKey = sdkKey; - this.hooks = configuration.Hooks; + this.hooks = options.Hooks; this.hooks.SetSender(this); - this.logger = new LoggerWrapper(configuration.Logger ?? ConfigCatClientOptions.CreateDefaultLogger(), this.hooks); + this.logger = new LoggerWrapper(options.Logger ?? ConfigCatClientOptions.CreateDefaultLogger(), this.hooks); this.configEvaluator = new RolloutEvaluator(this.logger); var cacheParameters = new CacheParameters ( - configCache: configuration.ConfigCache is not null - ? new ExternalConfigCache(configuration.ConfigCache, this.logger) + configCache: options.ConfigCache is not null + ? new ExternalConfigCache(options.ConfigCache, this.logger) : ConfigCatClientOptions.CreateDefaultConfigCache(), cacheKey: GetCacheKey(sdkKey) ); - if (configuration.FlagOverrides is not null) + if (options.FlagOverrides is not null) { - this.overrideDataSource = configuration.FlagOverrides.BuildDataSource(this.logger); - this.overrideBehaviour = configuration.FlagOverrides.OverrideBehaviour; + this.overrideDataSource = options.FlagOverrides.BuildDataSource(this.logger); + this.overrideBehaviour = options.FlagOverrides.OverrideBehaviour; } - this.defaultUser = configuration.DefaultUser; + this.defaultUser = options.DefaultUser; - var pollingMode = configuration.PollingMode ?? ConfigCatClientOptions.CreateDefaultPollingMode(); + var pollingMode = options.PollingMode ?? ConfigCatClientOptions.CreateDefaultPollingMode(); this.configService = this.overrideBehaviour != OverrideBehaviour.LocalOnly ? DetermineConfigService(pollingMode, - new HttpConfigFetcher(configuration.CreateUri(sdkKey), + new HttpConfigFetcher(options.CreateUri(sdkKey), $"{pollingMode.Identifier}-{Version}", this.logger, - configuration.HttpClientHandler, - configuration.IsCustomBaseUrl, - configuration.HttpTimeout), + options.HttpClientHandler, + options.IsCustomBaseUrl, + options.HttpTimeout), cacheParameters, this.logger, - configuration.Offline, + options.Offline, this.hooks) : new NullConfigService(this.logger, this.hooks); } - /// - /// For testing purposes only - /// + // For test purposes only internal ConfigCatClient(IConfigService configService, IConfigCatLogger logger, IRolloutEvaluator evaluator, Hooks? hooks = null) { if (hooks is not null) @@ -112,8 +110,8 @@ internal ConfigCatClient(IConfigService configService, IConfigCatLogger logger, /// Otherwise, the already created and configured instance is returned (in which case is ignored). /// So, please keep in mind that when you make multiple calls to this method using the same SDK Key, you may end up with multiple references to the same client object. /// - /// SDK Key to access configuration. (For the moment, SDK Key can also be set using . This setting will, however, be ignored and will be used, regardless.) - /// The configuration action. + /// SDK Key to access the ConfigCat config. + /// The action used to configure the client. /// is . /// is an empty string. public static IConfigCatClient Get(string sdkKey, Action? configurationAction = null) @@ -128,10 +126,10 @@ public static IConfigCatClient Get(string sdkKey, Action throw new ArgumentException("SDK Key cannot be empty.", nameof(sdkKey)); } - var configuration = new ConfigCatClientOptions(); - configurationAction?.Invoke(configuration); + var options = new ConfigCatClientOptions(); + configurationAction?.Invoke(options); - var instance = Instances.GetOrCreate(sdkKey, configuration, out var instanceAlreadyCreated); + var instance = Instances.GetOrCreate(sdkKey, options, out var instanceAlreadyCreated); if (instanceAlreadyCreated && configurationAction is not null) { @@ -144,7 +142,7 @@ public static IConfigCatClient Get(string sdkKey, Action /// ~ConfigCatClient() { - // Safeguard against situations where user forgets to dispose of the client instance. + // Safeguard against situations where user forgets to dispose the client instance. if (this.sdkKey is not null) { @@ -199,7 +197,7 @@ public void Dispose() } /// - /// Disposes of all existing instances. + /// Disposes all existing instances. /// /// Potential exceptions thrown by of the individual clients. public static void DisposeAll() @@ -662,7 +660,7 @@ private static IConfigService DetermineConfigService(PollingMode pollingMode, Ht logger, isOffline, hooks), - _ => throw new ArgumentException("Invalid configuration type."), + _ => throw new ArgumentException("Invalid polling mode.", nameof(pollingMode)), }; } diff --git a/src/ConfigCatClient/ConfigCatClient.csproj b/src/ConfigCatClient/ConfigCatClient.csproj index e75645e0..76dbe236 100644 --- a/src/ConfigCatClient/ConfigCatClient.csproj +++ b/src/ConfigCatClient/ConfigCatClient.csproj @@ -17,7 +17,7 @@ 10.0 enable nullable - https://github.com/configcat/.net-sdk/blob/master/CHANGELOG.md + https://github.com/configcat/.net-sdk/releases feature flag toggle feature-flag feature-flags featureflag featureflags feature-toggle feature-toggles featuretoggle featuretoggles canary release remote config remoteconfig remote-config configcat configcatclient Feature Flags created by developers for developers with ❤️. @@ -33,6 +33,11 @@ README.md true Debug;Release;Benchmark + + true + true + snupkg + true @@ -76,6 +81,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/ConfigCatClient/ConfigCatClientCache.cs b/src/ConfigCatClient/ConfigCatClientCache.cs index 7c89ecbb..a06fec96 100644 --- a/src/ConfigCatClient/ConfigCatClientCache.cs +++ b/src/ConfigCatClient/ConfigCatClientCache.cs @@ -9,7 +9,7 @@ internal sealed class ConfigCatClientCache { internal readonly Dictionary> instances = new(); - public ConfigCatClient GetOrCreate(string sdkKey, ConfigCatClientOptions configuration, out bool instanceAlreadyCreated) + public ConfigCatClient GetOrCreate(string sdkKey, ConfigCatClientOptions options, out bool instanceAlreadyCreated) { lock (this.instances) { @@ -18,14 +18,14 @@ public ConfigCatClient GetOrCreate(string sdkKey, ConfigCatClientOptions configu if (!this.instances.TryGetValue(sdkKey, out var weakRef)) { instanceAlreadyCreated = false; - instance = new ConfigCatClient(sdkKey, configuration); + instance = new ConfigCatClient(sdkKey, options); weakRef = new WeakReference(instance); this.instances.Add(sdkKey, weakRef); } else if (!weakRef.TryGetTarget(out instance)) { instanceAlreadyCreated = false; - instance = new ConfigCatClient(sdkKey, configuration); + instance = new ConfigCatClient(sdkKey, options); weakRef.SetTarget(instance); } else diff --git a/src/ConfigCatClient/ConfigService/AutoPollConfigService.cs b/src/ConfigCatClient/ConfigService/AutoPollConfigService.cs index 4be03151..346e93b4 100644 --- a/src/ConfigCatClient/ConfigService/AutoPollConfigService.cs +++ b/src/ConfigCatClient/ConfigService/AutoPollConfigService.cs @@ -14,17 +14,17 @@ internal sealed class AutoPollConfigService : ConfigServiceBase, IConfigService private CancellationTokenSource? timerCancellationTokenSource = new(); // used for signalling background work to stop internal AutoPollConfigService( - AutoPoll configuration, + AutoPoll options, IConfigFetcher configFetcher, CacheParameters cacheParameters, LoggerWrapper logger, bool isOffline = false, - Hooks? hooks = null) : this(configuration, configFetcher, cacheParameters, logger, startTimer: true, isOffline, hooks) + Hooks? hooks = null) : this(options, configFetcher, cacheParameters, logger, startTimer: true, isOffline, hooks) { } // For test purposes only internal AutoPollConfigService( - AutoPoll configuration, + AutoPoll options, IConfigFetcher configFetcher, CacheParameters cacheParameters, LoggerWrapper logger, @@ -32,15 +32,15 @@ internal AutoPollConfigService( bool isOffline = false, Hooks? hooks = null) : base(configFetcher, cacheParameters, logger, isOffline, hooks) { - this.pollInterval = configuration.PollInterval; - this.maxInitWaitTime = configuration.MaxInitWaitTime >= TimeSpan.Zero ? configuration.MaxInitWaitTime : Timeout.InfiniteTimeSpan; + this.pollInterval = options.PollInterval; + this.maxInitWaitTime = options.MaxInitWaitTime >= TimeSpan.Zero ? options.MaxInitWaitTime : Timeout.InfiniteTimeSpan; this.initializationCancellationTokenSource.Token.Register(this.Hooks.RaiseClientReady, useSynchronizationContext: false); - if (configuration.MaxInitWaitTime > TimeSpan.Zero) + if (options.MaxInitWaitTime > TimeSpan.Zero) { - this.initializationCancellationTokenSource.CancelAfter(configuration.MaxInitWaitTime); + this.initializationCancellationTokenSource.CancelAfter(options.MaxInitWaitTime); } - else if (configuration.MaxInitWaitTime == TimeSpan.Zero) + else if (options.MaxInitWaitTime == TimeSpan.Zero) { this.initializationCancellationTokenSource.Cancel(); } @@ -87,7 +87,7 @@ private void SignalInitialization() { // Since SignalInitialization and Dispose are not synchronized, // in extreme conditions a call to SignalInitialization may slip past the disposal of initializationCancellationTokenSource. - // In such cases we get an ObjectDisposedException here, which means that the config service has been disposed of in the meantime. + // In such cases we get an ObjectDisposedException here, which means that the config service has been disposed in the meantime. // Thus, we can safely swallow this exception. } } @@ -130,7 +130,7 @@ public ProjectConfig GetConfig() return this.ConfigCache.Get(base.CacheKey); } - public async Task GetConfigAsync(CancellationToken cancellationToken = default) + public async ValueTask GetConfigAsync(CancellationToken cancellationToken = default) { if (!IsOffline && !IsInitialized) { @@ -205,7 +205,7 @@ private void StartScheduler() }); } - private async Task PollCoreAsync(bool isFirstIteration, CancellationToken cancellationToken) + private async ValueTask PollCoreAsync(bool isFirstIteration, CancellationToken cancellationToken) { if (isFirstIteration) { diff --git a/src/ConfigCatClient/ConfigService/ConfigServiceBase.cs b/src/ConfigCatClient/ConfigService/ConfigServiceBase.cs index 9645b799..12450c0d 100644 --- a/src/ConfigCatClient/ConfigService/ConfigServiceBase.cs +++ b/src/ConfigCatClient/ConfigService/ConfigServiceBase.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using ConfigCat.Client.Cache; @@ -107,7 +106,7 @@ protected ConfigWithFetchResult RefreshConfigCore(ProjectConfig latestConfig) return new ConfigWithFetchResult(latestConfig, fetchResult); } - public virtual async Task RefreshConfigAsync(CancellationToken cancellationToken = default) + public virtual async ValueTask RefreshConfigAsync(CancellationToken cancellationToken = default) { if (!IsOffline) { diff --git a/src/ConfigCatClient/ConfigService/IConfigService.cs b/src/ConfigCatClient/ConfigService/IConfigService.cs index 7623ba31..7e7c6265 100644 --- a/src/ConfigCatClient/ConfigService/IConfigService.cs +++ b/src/ConfigCatClient/ConfigService/IConfigService.cs @@ -1,4 +1,3 @@ -using System; using System.Threading; using System.Threading.Tasks; @@ -8,11 +7,11 @@ internal interface IConfigService { ProjectConfig GetConfig(); - Task GetConfigAsync(CancellationToken cancellationToken = default); + ValueTask GetConfigAsync(CancellationToken cancellationToken = default); RefreshResult RefreshConfig(); - Task RefreshConfigAsync(CancellationToken cancellationToken = default); + ValueTask RefreshConfigAsync(CancellationToken cancellationToken = default); bool IsOffline { get; } diff --git a/src/ConfigCatClient/ConfigService/LazyLoadConfigService.cs b/src/ConfigCatClient/ConfigService/LazyLoadConfigService.cs index f0620d41..4dc4ab88 100644 --- a/src/ConfigCatClient/ConfigService/LazyLoadConfigService.cs +++ b/src/ConfigCatClient/ConfigService/LazyLoadConfigService.cs @@ -38,7 +38,7 @@ public ProjectConfig GetConfig() return cachedConfig; } - public async Task GetConfigAsync(CancellationToken cancellationToken = default) + public async ValueTask GetConfigAsync(CancellationToken cancellationToken = default) { var cachedConfig = await this.ConfigCache.GetAsync(base.CacheKey, cancellationToken).ConfigureAwait(false); diff --git a/src/ConfigCatClient/ConfigService/ManualPollConfigService.cs b/src/ConfigCatClient/ConfigService/ManualPollConfigService.cs index 013d53cc..33290ce2 100644 --- a/src/ConfigCatClient/ConfigService/ManualPollConfigService.cs +++ b/src/ConfigCatClient/ConfigService/ManualPollConfigService.cs @@ -1,4 +1,3 @@ -using System; using System.Threading; using System.Threading.Tasks; using ConfigCat.Client.Cache; @@ -18,7 +17,7 @@ public ProjectConfig GetConfig() return this.ConfigCache.Get(base.CacheKey); } - public Task GetConfigAsync(CancellationToken cancellationToken = default) + public ValueTask GetConfigAsync(CancellationToken cancellationToken = default) { return this.ConfigCache.GetAsync(base.CacheKey, cancellationToken); } diff --git a/src/ConfigCatClient/ConfigService/NullConfigService.cs b/src/ConfigCatClient/ConfigService/NullConfigService.cs index ec27e3ab..8dc4355b 100644 --- a/src/ConfigCatClient/ConfigService/NullConfigService.cs +++ b/src/ConfigCatClient/ConfigService/NullConfigService.cs @@ -16,11 +16,11 @@ public NullConfigService(LoggerWrapper logger, Hooks? hooks = null) public ProjectConfig GetConfig() => ProjectConfig.Empty; - public Task GetConfigAsync(CancellationToken cancellationToken = default) => Task.FromResult(ProjectConfig.Empty); + public ValueTask GetConfigAsync(CancellationToken cancellationToken = default) => new ValueTask(ProjectConfig.Empty); public RefreshResult RefreshConfig() { return RefreshResult.Failure($"Client is configured to use the {nameof(OverrideBehaviour.LocalOnly)} override behavior, which prevents making HTTP requests."); } - public Task RefreshConfigAsync(CancellationToken cancellationToken = default) => Task.FromResult(RefreshConfig()); + public ValueTask RefreshConfigAsync(CancellationToken cancellationToken = default) => new ValueTask(RefreshConfig()); public bool IsOffline => true; diff --git a/src/ConfigCatClient/Configuration/ConfigCatClientOptions.cs b/src/ConfigCatClient/Configuration/ConfigCatClientOptions.cs index 638d3aa4..5ebfdf4a 100644 --- a/src/ConfigCatClient/Configuration/ConfigCatClientOptions.cs +++ b/src/ConfigCatClient/Configuration/ConfigCatClientOptions.cs @@ -5,7 +5,7 @@ namespace ConfigCat.Client.Configuration; /// -/// Represents the ConfigCat SDK's configuration options. +/// Options used to configure the ConfigCat SDK. /// public class ConfigCatClientOptions : IProvidesHooks { @@ -16,7 +16,8 @@ public class ConfigCatClientOptions : IProvidesHooks internal static readonly Uri BaseUrlEu = new("https://cdn-eu.configcat.com"); /// - /// Logger instance. If not set, with Warning log level will be used by default. + /// The logger implementation to use for performing logging. + /// If not set, with will be used by default.
/// If you want to use custom logging instead, you can provide an implementation of . ///
public IConfigCatLogger? Logger { get; set; } @@ -24,7 +25,8 @@ public class ConfigCatClientOptions : IProvidesHooks internal static IConfigCatLogger CreateDefaultLogger() => new ConsoleLogger(LogLevel.Warning); /// - /// Cache instance. If not set, will be used by default. + /// The cache implementation to use for storing and retrieving downloaded config data. + /// If not set, will be used by default.
/// If you want to use custom caching instead, you can provide an implementation of . ///
public IConfigCatCache? ConfigCache { get; set; } @@ -32,7 +34,7 @@ public class ConfigCatClientOptions : IProvidesHooks internal static ConfigCache CreateDefaultConfigCache() => new InMemoryConfigCache(); /// - /// Polling mode. + /// The polling mode to use. /// If not set, will be used by default. /// public PollingMode? PollingMode { get; set; } @@ -40,14 +42,16 @@ public class ConfigCatClientOptions : IProvidesHooks internal static PollingMode CreateDefaultPollingMode() => PollingModes.AutoPoll(); /// - /// to provide network credentials and proxy settings. + /// An optional for providing network credentials and proxy settings. /// public HttpClientHandler? HttpClientHandler { get; set; } private Uri baseUrl = BaseUrlGlobal; /// - /// You can set a BaseUrl if you want to use a proxy server between your application and ConfigCat. + /// The base URL of the remote server providing the latest version of the config. + /// Defaults to the URL of the ConfigCat CDN.
+ /// If you want to use a proxy server between your application and ConfigCat, you need to set this property to the proxy URL. ///
public Uri BaseUrl { @@ -58,7 +62,7 @@ public Uri BaseUrl internal bool IsCustomBaseUrl => BaseUrl != BaseUrlGlobal && BaseUrl != BaseUrlEu; /// - /// Set this parameter to be in sync with the Data Governance preference on the Dashboard: + /// Set this property to be in sync with the Data Governance preference on the Dashboard: /// https://app.configcat.com/organization/data-governance (only Organization Admins have access). /// Defaults to . /// @@ -70,12 +74,12 @@ public Uri BaseUrl public TimeSpan HttpTimeout { get; set; } = TimeSpan.FromSeconds(30); /// - /// Feature flag and setting overrides. + /// The flag override to use. If not set, no flag override will be used. /// public FlagOverrides? FlagOverrides { get; set; } /// - /// The default user, used as fallback when there's no user parameter is passed to the , , etc. methods. + /// The default user, used as fallback when there's no user parameter is passed to the setting evaluation methods like , , etc. /// public User? DefaultUser { get; set; } diff --git a/src/ConfigCatClient/Configuration/DataGovernance.cs b/src/ConfigCatClient/Configuration/DataGovernance.cs index 2cc6a0bb..03b9d1cc 100644 --- a/src/ConfigCatClient/Configuration/DataGovernance.cs +++ b/src/ConfigCatClient/Configuration/DataGovernance.cs @@ -1,16 +1,16 @@ namespace ConfigCat.Client; /// -/// Control the location of the config.json files containing your feature flags and settings within the ConfigCat CDN. +/// Controls the location of the config JSON files containing your feature flags and settings within the ConfigCat CDN. /// public enum DataGovernance : byte { /// - /// Select this if your feature flags are published to all global CDN nodes. + /// Choose this option if your config JSON files are published to all global CDN nodes. /// Global = 0, /// - /// Select this if your feature flags are published to CDN nodes only in the EU. + /// Choose this option if your config JSON files are published to CDN nodes only in the EU. /// EuOnly = 1 } diff --git a/src/ConfigCatClient/Configuration/PollingMode.cs b/src/ConfigCatClient/Configuration/PollingMode.cs index d8728c0b..0aa11f50 100644 --- a/src/ConfigCatClient/Configuration/PollingMode.cs +++ b/src/ConfigCatClient/Configuration/PollingMode.cs @@ -3,7 +3,7 @@ namespace ConfigCat.Client.Configuration; /// -/// Represents the base class for the polling modes. +/// Defines the base class for polling modes. /// public abstract class PollingMode { @@ -13,14 +13,14 @@ private protected PollingMode() { } } /// -/// AutoPoll configuration settings object for +/// Represents the Auto Polling mode along with its settings. /// public class AutoPoll : PollingMode { internal override string Identifier => "a"; /// - /// Configuration refresh period. + /// Config refresh interval. /// public TimeSpan PollInterval { get; } @@ -45,7 +45,7 @@ internal AutoPoll(TimeSpan pollInterval, TimeSpan maxInitWaitTime) } /// -/// LazyLoad configuration settings object for +/// Represents the Lazy Loading mode along with its settings. /// public class LazyLoad : PollingMode { @@ -68,7 +68,7 @@ internal LazyLoad(TimeSpan cacheTimeToLive) } /// -/// ManualPoll configuration settings object for +/// Represents the Manual Polling mode. /// public class ManualPoll : PollingMode { diff --git a/src/ConfigCatClient/Configuration/PollingModes.cs b/src/ConfigCatClient/Configuration/PollingModes.cs index 8eeb1053..613380a9 100644 --- a/src/ConfigCatClient/Configuration/PollingModes.cs +++ b/src/ConfigCatClient/Configuration/PollingModes.cs @@ -4,38 +4,43 @@ namespace ConfigCat.Client; /// -/// Contains the supported polling modes. +/// Provides static factory methods for the supported polling modes. /// public static class PollingModes { /// - /// Constructs a new auto polling mode. + /// Creates an instance of the class with the specified settings. /// /// - /// Configuration refresh period. + /// Config refresh interval. + /// Specifies how frequently the locally cached config will be refreshed by fetching the latest version from the remote server.
/// (Default value is 60 seconds. Minimum value is 1 second. Maximum value is milliseconds.) /// /// - /// Maximum waiting time between initialization and the first config acquisition. + /// Maximum waiting time between initialization and the first config acquisition.
/// (Default value is 5 seconds. Maximum value is milliseconds. Negative values mean infinite waiting.) /// - /// The auto polling mode. + /// The new instance. + /// is outside the allowed range. + /// is outside the allowed range. public static AutoPoll AutoPoll(TimeSpan? pollInterval = null, TimeSpan? maxInitWaitTime = null) => new(pollInterval ?? TimeSpan.FromSeconds(60), maxInitWaitTime ?? TimeSpan.FromSeconds(5)); /// - /// Constructs a new lazy load polling mode. + /// Creates an instance of the class with the specified settings. /// /// /// Cache time to live value. + /// Specifies how long the locally cached config can be used before refreshing it again by fetching the latest version from the remote server.
/// (Default value is 60 seconds. Minimum value is 1 second. Maximum value is seconds.) /// - /// The lazy load polling mode. + /// The new instance. + /// is outside the allowed range. public static LazyLoad LazyLoad(TimeSpan? cacheTimeToLive = null) => new(cacheTimeToLive ?? TimeSpan.FromSeconds(60)); /// - /// The manual polling mode. + /// Provides an instance of the class. /// public static readonly ManualPoll ManualPoll = new(); } diff --git a/src/ConfigCatClient/Evaluation/EvaluationDetails.cs b/src/ConfigCatClient/Evaluation/EvaluationDetails.cs index aec8bcea..68acdb43 100644 --- a/src/ConfigCatClient/Evaluation/EvaluationDetails.cs +++ b/src/ConfigCatClient/Evaluation/EvaluationDetails.cs @@ -1,6 +1,5 @@ using System; using System.Diagnostics; -using ConfigCat.Client.Evaluation; #if USE_NEWTONSOFT_JSON using JsonValue = Newtonsoft.Json.Linq.JValue; @@ -10,7 +9,7 @@ namespace ConfigCat.Client; -internal delegate EvaluationDetails EvaluationDetailsFactory(SettingType settingType, JsonValue value); +internal delegate EvaluationDetails EvaluationDetailsFactory(Setting setting, JsonValue value); /// /// The evaluated value and additional information about the evaluation of a feature flag or setting. @@ -22,12 +21,12 @@ private static EvaluationDetails Create(JsonValue value) return new EvaluationDetails { Value = value.ConvertTo() }; } - internal static EvaluationDetails Create(SettingType settingType, JsonValue value) + 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."); - // SettingType was not specified in the config.json? + // 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) { // Let's try to infer it from the JSON value. @@ -35,7 +34,7 @@ internal static EvaluationDetails Create(SettingType settingType if (settingType == SettingType.Unknown) { - throw new ArgumentException($"The type of setting value '{value}' is not supported.", nameof(value)); + throw new ArgumentException(unsupportedTypeError ?? $"Setting value '{value}' is of an unsupported type.", nameof(value)); } } @@ -69,7 +68,7 @@ internal static EvaluationDetails Create(SettingType settingType, JsonValue valu internal static EvaluationDetails FromJsonValue( EvaluationDetailsFactory factory, - SettingType settingType, + Setting setting, string key, JsonValue value, string? variationId, @@ -78,7 +77,7 @@ internal static EvaluationDetails FromJsonValue( RolloutRule? matchedEvaluationRule = null, RolloutPercentageItem? matchedEvaluationPercentageRule = null) { - var instance = factory(settingType, value); + var instance = factory(setting, value); instance.Key = key; instance.VariationId = variationId; @@ -137,17 +136,17 @@ private protected EvaluationDetails(string key) public string? VariationId { get; set; } /// - /// Time of last successful download of config.json (or if there has been no successful download yet). + /// Time of last successful config download (or if there has been no successful download yet). /// public DateTime FetchTime { get; set; } = DateTime.MinValue; /// - /// The object used for the evaluation (if available). + /// The User Object used for the evaluation (if available). /// public User? User { get; set; } /// - /// Indicates whether the default value passed to or + /// Indicates whether the default value passed to the setting evaluation methods like , , etc. /// is used as the result of the evaluation. /// public bool IsDefaultValue { get; set; } @@ -179,7 +178,7 @@ public sealed record class EvaluationDetails : EvaluationDetails internal EvaluationDetails() : this(key: null!, value: default!) { } /// - /// Creates an instance of . + /// Initializes a new instance of the class. /// public EvaluationDetails(string key, TValue value) : base(key) { diff --git a/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs b/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs index 12f2de50..6fac79c8 100644 --- a/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs +++ b/src/ConfigCatClient/Evaluation/RolloutEvaluator.cs @@ -46,7 +46,7 @@ public EvaluationDetails Evaluate(Setting setting, string key, string? logDefaul return EvaluationDetails.FromJsonValue( detailsFactory, - setting.SettingType, + setting, key, evaluateRulesResult.Value, evaluateRulesResult.VariationId, @@ -64,7 +64,7 @@ public EvaluationDetails Evaluate(Setting setting, string key, string? logDefaul return EvaluationDetails.FromJsonValue( detailsFactory, - setting.SettingType, + setting, key, evaluatePercentageRulesResult.Value, evaluatePercentageRulesResult.VariationId, @@ -85,7 +85,7 @@ public EvaluationDetails Evaluate(Setting setting, string key, string? logDefaul return EvaluationDetails.FromJsonValue( detailsFactory, - setting.SettingType, + setting, key, setting.Value, setting.VariationId, diff --git a/src/ConfigCatClient/Evaluation/RolloutEvaluatorExtensions.cs b/src/ConfigCatClient/Evaluation/RolloutEvaluatorExtensions.cs index d76dc2e9..292991e7 100644 --- a/src/ConfigCatClient/Evaluation/RolloutEvaluatorExtensions.cs +++ b/src/ConfigCatClient/Evaluation/RolloutEvaluatorExtensions.cs @@ -14,7 +14,7 @@ public static EvaluationDetails Evaluate(this IRolloutEvaluator evaluator, { var logDefaultValue = defaultValue is not null ? Convert.ToString(defaultValue, CultureInfo.InvariantCulture) : null; return (EvaluationDetails)evaluator.Evaluate(setting, key, logDefaultValue, user, remoteConfig, - static (settingType, value) => EvaluationDetails.Create(settingType, value)); + static (setting, value) => EvaluationDetails.Create(setting.SettingType, value, setting.UnsupportedTypeError)); } public static EvaluationDetails Evaluate(this IRolloutEvaluator evaluator, IReadOnlyDictionary? settings, string key, T defaultValue, User? user, @@ -42,7 +42,7 @@ public static EvaluationDetails Evaluate(this IRolloutEvaluator evaluator, Setti { var logDefaultValue = defaultValue is not null ? Convert.ToString(defaultValue, CultureInfo.InvariantCulture) : null; return evaluator.Evaluate(setting, key, logDefaultValue, user, remoteConfig, - static (settingType, value) => EvaluationDetails.Create(settingType, value)); + static (setting, value) => EvaluationDetails.Create(setting.SettingType, value)); } public static EvaluationDetails[] EvaluateAll(this IRolloutEvaluator evaluator, IReadOnlyDictionary? settings, User? user, diff --git a/src/ConfigCatClient/Extensions/HttpContentExtensions.cs b/src/ConfigCatClient/Extensions/HttpContentExtensions.cs deleted file mode 100644 index 7aa20e09..00000000 --- a/src/ConfigCatClient/Extensions/HttpContentExtensions.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.IO; -using System.Threading; - -namespace System.Net.Http; - -#if NET5_0_OR_GREATER -internal static class HttpContentExtensions -{ - public static string ReadAsString(this HttpContent content, CancellationToken token) - { - using var responseStream = content.ReadAsStream(token); - using var reader = new StreamReader(responseStream); - return reader.ReadToEnd(); - } -} -#endif diff --git a/src/ConfigCatClient/Extensions/ObjectExtensions.cs b/src/ConfigCatClient/Extensions/ObjectExtensions.cs index abfa32ec..c41e1087 100644 --- a/src/ConfigCatClient/Extensions/ObjectExtensions.cs +++ b/src/ConfigCatClient/Extensions/ObjectExtensions.cs @@ -116,19 +116,35 @@ TypeCode.Double when IsWithinAllowedDoubleRange((IConvertible)value) => public static Setting ToSetting(this object? value) { var settingType = DetermineSettingType(value); - if (settingType == SettingType.Unknown) + + JsonValue jsonValue; + string? unsupportedTypeError; + if (settingType != SettingType.Unknown) { - throw new ArgumentException($"Could not determine the setting type of {value ?? "(null)"}."); +#if USE_NEWTONSOFT_JSON + jsonValue = new Newtonsoft.Json.Linq.JValue(value); +#else + jsonValue = Text.Json.JsonSerializer.SerializeToElement(value); +#endif + unsupportedTypeError = null; } - - return new Setting + else { #if USE_NEWTONSOFT_JSON - Value = new Newtonsoft.Json.Linq.JValue(value), + jsonValue = JsonValue.CreateUndefined(); #else - Value = Text.Json.JsonSerializer.SerializeToElement(value), + jsonValue = default; #endif - SettingType = settingType + unsupportedTypeError = value is not null + ? $"Setting value '{value}' is of an unsupported type ({value.GetType()})." + : $"Setting value is null."; + } + + return new Setting + { + Value = jsonValue, + SettingType = settingType, + UnsupportedTypeError = unsupportedTypeError, }; } diff --git a/src/ConfigCatClient/Extensions/SerializationExtensions.cs b/src/ConfigCatClient/Extensions/SerializationExtensions.cs index 35725c06..a4612c65 100644 --- a/src/ConfigCatClient/Extensions/SerializationExtensions.cs +++ b/src/ConfigCatClient/Extensions/SerializationExtensions.cs @@ -1,7 +1,6 @@ -using System; #if USE_NEWTONSOFT_JSON -using Newtonsoft.Json; using System.IO; +using Newtonsoft.Json; #else using System.Text.Json; using System.Text.Json.Serialization; diff --git a/src/ConfigCatClient/Extensions/TypeExtensions.cs b/src/ConfigCatClient/Extensions/TypeExtensions.cs index da1aa419..becb66a8 100644 --- a/src/ConfigCatClient/Extensions/TypeExtensions.cs +++ b/src/ConfigCatClient/Extensions/TypeExtensions.cs @@ -8,7 +8,7 @@ public static void EnsureSupportedSettingClrType(this Type type, string paramNam { if (type != typeof(object) && type.ToSettingType() == SettingType.Unknown) { - throw new ArgumentException($"Only the following types are supported: {typeof(string)}, {typeof(bool)}, {typeof(int)}, {typeof(long)}, {typeof(double)} and {typeof(object)} (both nullable and non-nullable).", paramName); + throw new ArgumentException($"Only the following types are supported: {typeof(string)}, {typeof(bool)}, {typeof(int)}, {typeof(long)}, {typeof(double)} and {typeof(object)} (both nullable and non-nullable).", paramName); } } diff --git a/src/ConfigCatClient/Hooks/IProvidesHooks.cs b/src/ConfigCatClient/Hooks/IProvidesHooks.cs index 36ad0d3f..f58d4690 100644 --- a/src/ConfigCatClient/Hooks/IProvidesHooks.cs +++ b/src/ConfigCatClient/Hooks/IProvidesHooks.cs @@ -18,7 +18,7 @@ public interface IProvidesHooks event EventHandler? FlagEvaluated; /// - /// Occurs after the configuration has been updated. + /// Occurs after the locally cached config has been updated. /// event EventHandler? ConfigChanged; diff --git a/src/ConfigCatClient/Hooks/NullHooks.cs b/src/ConfigCatClient/Hooks/NullHooks.cs index 72f76b33..909e9cb9 100644 --- a/src/ConfigCatClient/Hooks/NullHooks.cs +++ b/src/ConfigCatClient/Hooks/NullHooks.cs @@ -1,5 +1,3 @@ -using System; - namespace ConfigCat.Client; internal sealed class NullHooks : Hooks diff --git a/src/ConfigCatClient/IConfigCatClient.cs b/src/ConfigCatClient/IConfigCatClient.cs index 3ec860d8..385993ac 100644 --- a/src/ConfigCatClient/IConfigCatClient.cs +++ b/src/ConfigCatClient/IConfigCatClient.cs @@ -6,25 +6,26 @@ namespace ConfigCat.Client; /// -/// Provides client definition for +/// Defines the public interface of the class. /// public interface IConfigCatClient : IProvidesHooks, IDisposable { /// - /// Sets or gets the logging level. + /// Gets or sets the log level (the minimum level to use for filtering log events). /// LogLevel LogLevel { get; set; } /// - /// Returns a value for the key. (Key for programs) + /// Returns the value of a feature flag or setting identified by synchronously. /// /// - /// Setting type. Only the following types are allowed: - /// , , , , and (both nullable and non-nullable). + /// The type of the value. Only the following types are allowed: + /// , , , , and (both nullable and non-nullable).
+ /// The type must correspond to the setting type, otherwise will be returned. ///
- /// Key for programs - /// In case of failure return this value - /// The user object for variation evaluation + /// Key of the feature flag or setting. + /// In case of failure, this value will be returned. + /// The User Object to use for evaluating targeting rules and percentage options. /// The value of the feature flag or setting. /// is . /// is an empty string. @@ -32,17 +33,18 @@ public interface IConfigCatClient : IProvidesHooks, IDisposable T GetValue(string key, T defaultValue, User? user = null); /// - /// Returns a value for the key. (Key for programs) + /// Returns the value of a feature flag or setting identified by asynchronously. /// /// - /// Setting type. Only the following types are allowed: - /// , , , , and (both nullable and non-nullable). + /// The type of the value. Only the following types are allowed: + /// , , , , and (both nullable and non-nullable).
+ /// The type must correspond to the setting type, otherwise will be returned. ///
- /// Key for programs. - /// In case of failure return this value. - /// The user object for variation evaluation. + /// Key of the feature flag or setting. + /// In case of failure, this value will be returned. + /// The User Object to use for evaluating targeting rules and percentage options. /// A to observe while waiting for the task to complete. - /// The task that will evaluate the value of the feature flag or setting. + /// A task that represents the asynchronous operation. The task result contains the value of the feature flag or setting. /// is . /// is an empty string. /// is not an allowed type. @@ -50,15 +52,16 @@ public interface IConfigCatClient : IProvidesHooks, IDisposable Task GetValueAsync(string key, T defaultValue, User? user = null, CancellationToken cancellationToken = default); /// - /// Returns the value along with evaluation details of a feature flag or setting by the given key. + /// Returns the value along with evaluation details of a feature flag or setting identified by synchronously. /// /// - /// Setting type. Only the following types are allowed: - /// , , , , and (both nullable and non-nullable). + /// The type of the value. Only the following types are allowed: + /// , , , , and (both nullable and non-nullable).
+ /// The type must correspond to the setting type, otherwise will be returned. ///
- /// Key for programs - /// In case of failure return this value - /// The user object for variation evaluation + /// Key of the feature flag or setting. + /// In case of failure, this value will be returned. + /// The User Object to use for evaluating targeting rules and percentage options. /// The value along with the details of evaluation of the feature flag or setting. /// is . /// is an empty string. @@ -66,17 +69,18 @@ public interface IConfigCatClient : IProvidesHooks, IDisposable EvaluationDetails GetValueDetails(string key, T defaultValue, User? user = null); /// - /// Returns the value along with evaluation details of a feature flag or setting by the given key. + /// Returns the value along with evaluation details of a feature flag or setting identified by asynchronously. /// /// - /// Setting type. Only the following types are allowed: - /// , , , , and (both nullable and non-nullable). + /// The type of the value. Only the following types are allowed: + /// , , , , and (both nullable and non-nullable).
+ /// The type must correspond to the setting type, otherwise will be returned. ///
- /// Key for programs - /// In case of failure return this value - /// The user object for variation evaluation + /// Key of the feature flag or setting. + /// In case of failure, this value will be returned. + /// The User Object to use for evaluating targeting rules and percentage options. /// A to observe while waiting for the task to complete. - /// The value along with the details of evaluation of the feature flag or setting. + /// A task that represents the asynchronous operation. The task result contains the value along with the details of evaluation of the feature flag or setting. /// is . /// is an empty string. /// is not an allowed type. @@ -84,72 +88,74 @@ public interface IConfigCatClient : IProvidesHooks, IDisposable Task> GetValueDetailsAsync(string key, T defaultValue, User? user = null, CancellationToken cancellationToken = default); /// - /// Returns a collection with all keys. + /// Returns all setting keys synchronously. /// - /// The key collection. + /// The collection of keys. IReadOnlyCollection GetAllKeys(); /// - /// Returns a collection with all keys asynchronously. + /// Returns all setting keys asynchronously. /// /// A to observe while waiting for the task to complete. - /// The key collection. + /// A task that represents the asynchronous operation. The task result contains the collection of keys. Task> GetAllKeysAsync(CancellationToken cancellationToken = default); /// - /// Returns the key-value collection of all feature flags and settings synchronously. + /// Returns the keys and values of all feature flags and settings synchronously. /// - /// The user object for variation evaluation. - /// The key-value collection. + /// The User Object to use for evaluating targeting rules and percentage options. + /// The dictionary containing the keys and values. IReadOnlyDictionary GetAllValues(User? user = null); /// - /// Returns the key-value collection of all feature flags and settings asynchronously. + /// Returns the keys and values of all feature flags and settings asynchronously. /// - /// The user object for variation evaluation. + /// The User Object to use for evaluating targeting rules and percentage options. /// A to observe while waiting for the task to complete. - /// The key-value collection. + /// A task that represents the asynchronous operation. The task result contains the dictionary containing the keys and values. Task> GetAllValuesAsync(User? user = null, CancellationToken cancellationToken = default); /// /// Returns the values along with evaluation details of all feature flags and settings synchronously. /// - /// The user object for variation evaluation. - /// The key-value collection. + /// The User Object to use for evaluating targeting rules and percentage options. + /// The list of values along with evaluation details. 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 User Object to use for evaluating targeting rules and percentage options. /// A to observe while waiting for the task to complete. - /// The key-value collection. + /// A task that represents the asynchronous operation. The task result contains the list of values along with evaluation details. Task> GetAllValueDetailsAsync(User? user = null, CancellationToken cancellationToken = default); /// - /// Refreshes the configuration. + /// Refreshes the locally cached config by fetching the latest version from the remote server synchronously. /// + /// The refresh result. RefreshResult ForceRefresh(); /// - /// Refreshes the configuration asynchronously. + /// Refreshes the locally cached config by fetching the latest version from the remote server asynchronously. /// /// A to observe while waiting for the task to complete. + /// A task that represents the asynchronous operation. The task result contains the refresh result. Task ForceRefreshAsync(CancellationToken cancellationToken = default); /// /// Sets the default user. /// - /// The default user object for variation evaluation. + /// The default User Object to use for evaluating targeting rules and percentage options. void SetDefaultUser(User user); /// - /// Sets the default user to null. + /// Clears the default user. /// void ClearDefaultUser(); /// - /// True when the client is configured not to initiate HTTP requests, otherwise false. + /// Returns when the client is configured not to initiate HTTP requests, otherwise . /// bool IsOffline { get; } @@ -159,7 +165,7 @@ public interface IConfigCatClient : IProvidesHooks, IDisposable void SetOnline(); /// - /// Configures the client to not initiate HTTP requests and work only from its cache. + /// Configures the client to not initiate HTTP requests and work using the locally cached config only. /// void SetOffline(); } diff --git a/src/ConfigCatClient/IConfigFetcher.cs b/src/ConfigCatClient/IConfigFetcher.cs index 00738ec1..ef6205fd 100644 --- a/src/ConfigCatClient/IConfigFetcher.cs +++ b/src/ConfigCatClient/IConfigFetcher.cs @@ -4,22 +4,22 @@ namespace ConfigCat.Client; /// -/// Defines configuration fetch +/// Defines the interface of the service used to fetch configs. /// internal interface IConfigFetcher { /// /// Fetches the configuration asynchronously. /// - /// Last fetched configuration if it is present + /// Last fetched configuration if it is present. /// A to observe while waiting for the task to complete. - /// The task that does the fetch. + /// A task that represents the asynchronous operation. The task result contains the fetched config. Task FetchAsync(ProjectConfig lastConfig, CancellationToken cancellationToken = default); /// /// Fetches the configuration synchronously. /// - /// Last fetched configuration if it is present + /// Last fetched configuration if it is present. /// The fetched config. FetchResult Fetch(ProjectConfig lastConfig); } diff --git a/src/ConfigCatClient/Logging/ConsoleLogger.cs b/src/ConfigCatClient/Logging/ConsoleLogger.cs index a2540ab8..c89f7462 100644 --- a/src/ConfigCatClient/Logging/ConsoleLogger.cs +++ b/src/ConfigCatClient/Logging/ConsoleLogger.cs @@ -4,22 +4,28 @@ namespace ConfigCat.Client; /// -/// Write log messages into +/// An implementation of which writes log messages into . /// public class ConsoleLogger : IConfigCatLogger { + private volatile LogLevel logLevel; + /// - public LogLevel LogLevel { get; set; } + public LogLevel LogLevel + { + get => this.logLevel; + set => this.logLevel = value; + } /// - /// Create a instance with Warning loglevel + /// Initializes a new instance of the class with . /// public ConsoleLogger() : this(LogLevel.Warning) { } /// - /// Create a instance + /// Initializes a new instance of the class with the specified log level. /// - /// Log level + /// Log level (the minimum level to use for filtering log events). public ConsoleLogger(LogLevel logLevel) { LogLevel = logLevel; diff --git a/src/ConfigCatClient/Logging/FormattableLogMessage.cs b/src/ConfigCatClient/Logging/FormattableLogMessage.cs index 6974c0ef..168e8355 100644 --- a/src/ConfigCatClient/Logging/FormattableLogMessage.cs +++ b/src/ConfigCatClient/Logging/FormattableLogMessage.cs @@ -22,7 +22,7 @@ internal static FormattableLogMessage FromInterpolated(FormattableString message } /// - /// Constructs a from a plain log message. + /// Initializes a new instance of the struct from a plain log message. /// public FormattableLogMessage(string message) { @@ -33,7 +33,7 @@ public FormattableLogMessage(string message) } /// - /// Constructs a from a log message format and the corresponding named arguments. + /// Initializes a new instance of the struct from a log message format and the corresponding named arguments. /// public FormattableLogMessage(string format, string[] argNames, object?[] argValues) { diff --git a/src/ConfigCatClient/Logging/IConfigCatLogger.cs b/src/ConfigCatClient/Logging/IConfigCatLogger.cs index e7eaf8ed..33161f8a 100644 --- a/src/ConfigCatClient/Logging/IConfigCatLogger.cs +++ b/src/ConfigCatClient/Logging/IConfigCatLogger.cs @@ -8,14 +8,14 @@ namespace ConfigCat.Client; public interface IConfigCatLogger { /// - /// Specifies message filtering. + /// Gets or sets the log level (the minimum level to use for filtering log events). /// LogLevel LogLevel { get; set; } /// - /// Writes a message into the log. + /// Writes an event into the log. /// - /// Level (event severity). + /// Event severity level. /// Event identifier. /// Message. /// The object related to the message (if any). diff --git a/src/ConfigCatClient/Logging/LogEventId.cs b/src/ConfigCatClient/Logging/LogEventId.cs index 5675ffc3..646132b7 100644 --- a/src/ConfigCatClient/Logging/LogEventId.cs +++ b/src/ConfigCatClient/Logging/LogEventId.cs @@ -33,7 +33,7 @@ public static implicit operator LogEventId(int id) } /// - /// Initializes an instance of the struct. + /// Initializes a new instance of the struct. /// /// The numeric identifier for the event. public LogEventId(int id) diff --git a/src/ConfigCatClient/Logging/LogLevel.cs b/src/ConfigCatClient/Logging/LogLevel.cs index 78a17a08..3ed5989d 100644 --- a/src/ConfigCatClient/Logging/LogLevel.cs +++ b/src/ConfigCatClient/Logging/LogLevel.cs @@ -1,29 +1,32 @@ namespace ConfigCat.Client; /// -/// Specifies message's filtering to output for the class. -/// Debug > Info > Warning > Error > Off +/// Specifies event severity levels for the interface. +/// The levels are interpreted as minimum levels in the case of event filtering. /// +/// +/// Debug > Info > Warning > Error > Off +/// public enum LogLevel { /// - /// No messages are logged. + /// No events are logged. /// Off = 0, /// - /// Error messages are logged. All other messages are discarded. + /// Error events are logged. All other events are discarded. /// Error = 1, /// - /// Warning and Error messages should be logged. Information and Debug messages are discarded. + /// Warning and Error events should be logged. Information and Debug events are discarded. /// Warning = 2, /// - /// Information, Warning and Error are logged. Debug messages are discarded. + /// Information, Warning and Error are logged. Debug events are discarded. /// Info = 3, /// - /// All messages should be logged. + /// All events are logged. /// Debug = 4 } diff --git a/src/ConfigCatClient/Models/Comparator.cs b/src/ConfigCatClient/Models/Comparator.cs index 14890f00..45264536 100644 --- a/src/ConfigCatClient/Models/Comparator.cs +++ b/src/ConfigCatClient/Models/Comparator.cs @@ -1,7 +1,7 @@ namespace ConfigCat.Client; /// -/// Operator of targeting rules. +/// Targeting rule comparison operator. /// public enum Comparator : byte { diff --git a/src/ConfigCatClient/Models/RolloutPercentageItem.cs b/src/ConfigCatClient/Models/RolloutPercentageItem.cs index 5526e235..64b8c42e 100644 --- a/src/ConfigCatClient/Models/RolloutPercentageItem.cs +++ b/src/ConfigCatClient/Models/RolloutPercentageItem.cs @@ -16,12 +16,12 @@ namespace ConfigCat.Client; public interface IPercentageOption { /// - /// The order value for determining the order of evaluation of rules. + /// A numeric value which determines the order of evaluation. /// short Order { get; } /// - /// The value associated with the targeting rule. + /// The value associated with the percentage option. /// object Value { get; } @@ -68,7 +68,7 @@ internal sealed class RolloutPercentageItem : IPercentageOption #endif public string? VariationId { get; set; } - /// > + /// public override string ToString() { var variationIdString = !string.IsNullOrEmpty(VariationId) ? " [" + VariationId + "]" : string.Empty; diff --git a/src/ConfigCatClient/Models/RolloutRule.cs b/src/ConfigCatClient/Models/RolloutRule.cs index 2046fc10..4128d9c3 100644 --- a/src/ConfigCatClient/Models/RolloutRule.cs +++ b/src/ConfigCatClient/Models/RolloutRule.cs @@ -16,12 +16,12 @@ namespace ConfigCat.Client; public interface ITargetingRule { /// - /// The order value for determining the order of evaluation of rules. + /// A numeric value which determines the order of evaluation. /// short Order { get; } /// - /// The attribute that the targeting rule is based on. Could be "User ID", "Email", "Country" or any custom attribute. + /// The attribute that the targeting rule is based on. Can be "User ID", "Email", "Country" or any custom attribute. /// string ComparisonAttribute { get; } @@ -31,7 +31,7 @@ public interface ITargetingRule Comparator Comparator { get; } /// - /// The value that the attribute is compared to. Could be a string, a number, a semantic version or a comma-separated list, depending on the comparator. + /// The value that the attribute is compared to. Can be a string, a number, a semantic version or a comma-separated list, depending on the comparator. /// string ComparisonValue { get; } @@ -118,7 +118,7 @@ internal static string FormatComparator(Comparator comparator) }; } - /// > + /// public override string ToString() { var variationIdString = !string.IsNullOrEmpty(VariationId) ? " [" + VariationId + "]" : string.Empty; diff --git a/src/ConfigCatClient/Models/Setting.cs b/src/ConfigCatClient/Models/Setting.cs index 73a9e5dd..0e93141a 100644 --- a/src/ConfigCatClient/Models/Setting.cs +++ b/src/ConfigCatClient/Models/Setting.cs @@ -19,7 +19,7 @@ namespace ConfigCat.Client; public interface ISetting { /// - /// The default value of the setting. + /// The (fallback) value of the setting. /// object Value { get; } @@ -100,4 +100,7 @@ public RolloutRule[] RolloutRules [JsonPropertyName("i")] #endif public string? VariationId { get; set; } + + [JsonIgnore] + public string? UnsupportedTypeError { get; set; } } diff --git a/src/ConfigCatClient/Override/FlagOverrides.cs b/src/ConfigCatClient/Override/FlagOverrides.cs index 88678b46..e8b9b51f 100644 --- a/src/ConfigCatClient/Override/FlagOverrides.cs +++ b/src/ConfigCatClient/Override/FlagOverrides.cs @@ -5,7 +5,7 @@ namespace ConfigCat.Client; /// -/// Describes feature flag and setting overrides. +/// Represents a flag override along with its settings. Also provides static factory methods for defining flag overrides. /// public class FlagOverrides { @@ -29,11 +29,9 @@ private FlagOverrides(IDictionary dictionary, bool watchChanges, } /// - /// The override behaviour. It can be used to set preference on whether the local values should - /// override the remote values, or use local values only when a remote value doesn't exist, - /// or use it for local only mode. + /// The override behaviour. /// - public OverrideBehaviour OverrideBehaviour { get; private set; } + public OverrideBehaviour OverrideBehaviour { get; } internal IOverrideDataSource BuildDataSource(LoggerWrapper logger) { @@ -47,31 +45,37 @@ internal IOverrideDataSource BuildDataSource(LoggerWrapper logger) } /// - /// Creates an override descriptor that uses a local file data source. + /// Creates an instance of the class that uses a local file data source. /// /// Path to the file. - /// When it's true, the file will be reloaded when it gets modified. - /// the override behaviour. It can be used to set preference on whether the local values should override the remote values, or use local values only when a remote value doesn't exist, or use it for local only mode. - /// The override descriptor. + /// If set to , the file will be reloaded when it gets modified. + /// The override behaviour. Specifies whether the local values should override the remote values + /// or local values should only be used when a remote value doesn't exist or the local values should be used only. + /// The new instance. + /// is . public static FlagOverrides LocalFile(string filePath, bool autoReload, OverrideBehaviour overrideBehaviour) => - new(filePath, autoReload, overrideBehaviour); + new(filePath ?? throw new ArgumentNullException(nameof(filePath)), autoReload, overrideBehaviour); /// - /// Creates an override descriptor that uses a dictionary data source. + /// Creates an instance of the class that uses a dictionary data source. /// - /// Dictionary that contains the overrides. - /// the override behaviour. It can be used to set preference on whether the local values should override the remote values, or use local values only when a remote value doesn't exist, or use it for local only mode. - /// The override descriptor. + /// The dictionary that contains the overrides. + /// The override behaviour. Specifies whether the local values should override the remote values + /// or local values should only be used when a remote value doesn't exist or the local values should be used only. + /// The new instance. + /// is . public static FlagOverrides LocalDictionary(IDictionary dictionary, OverrideBehaviour overrideBehaviour) => - new(dictionary, false, overrideBehaviour); + new(dictionary ?? throw new ArgumentNullException(nameof(dictionary)), false, overrideBehaviour); /// - /// Creates an override descriptor that uses a dictionary data source. + /// Creates an instance of the class that uses a dictionary data source. /// - /// Dictionary that contains the overrides. - /// Indicates whether the SDK should track the input dictionary for changes. - /// the override behaviour. It can be used to set preference on whether the local values should override the remote values, or use local values only when a remote value doesn't exist, or use it for local only mode. - /// The override descriptor. + /// The dictionary that contains the overrides. + /// If set to , the input dictionary will be tracked for changes. + /// The override behaviour. Specifies whether the local values should override the remote values + /// or local values should only be used when a remote value doesn't exist or the local values should be used only. + /// The new instance. + /// is . public static FlagOverrides LocalDictionary(IDictionary dictionary, bool watchChanges, OverrideBehaviour overrideBehaviour) => - new(dictionary, watchChanges, overrideBehaviour); + new(dictionary ?? throw new ArgumentNullException(nameof(dictionary)), watchChanges, overrideBehaviour); } diff --git a/src/ConfigCatClient/Override/IOverrideDataSource.cs b/src/ConfigCatClient/Override/IOverrideDataSource.cs index e341221e..31ea4f0e 100644 --- a/src/ConfigCatClient/Override/IOverrideDataSource.cs +++ b/src/ConfigCatClient/Override/IOverrideDataSource.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using ConfigCat.Client.Evaluation; namespace ConfigCat.Client.Override; diff --git a/src/ConfigCatClient/Override/LocalDictionaryDataSource.cs b/src/ConfigCatClient/Override/LocalDictionaryDataSource.cs index 92bbd8e7..55af9791 100644 --- a/src/ConfigCatClient/Override/LocalDictionaryDataSource.cs +++ b/src/ConfigCatClient/Override/LocalDictionaryDataSource.cs @@ -3,7 +3,6 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using ConfigCat.Client.Evaluation; namespace ConfigCat.Client.Override; diff --git a/src/ConfigCatClient/Override/LocalFileDataSource.cs b/src/ConfigCatClient/Override/LocalFileDataSource.cs index 5f3ba200..a9eaa3e3 100644 --- a/src/ConfigCatClient/Override/LocalFileDataSource.cs +++ b/src/ConfigCatClient/Override/LocalFileDataSource.cs @@ -5,7 +5,6 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using ConfigCat.Client.Evaluation; namespace ConfigCat.Client.Override; @@ -52,7 +51,7 @@ public LocalFileDataSource(string filePath, bool autoReload, LoggerWrapper logge private void StartWatch() { - // It's better to acquire a CancellationToken here because the getter might throw if CTS got disposed of. + // It's better to acquire a CancellationToken here because the getter might throw if CTS got disposed. var cancellationToken = this.cancellationTokenSource.Token; Task.Run(async () => @@ -86,7 +85,7 @@ private void StartWatch() }); } - private async Task WatchCoreAsync(CancellationToken cancellationToken) + private async ValueTask WatchCoreAsync(CancellationToken cancellationToken) { var lastWriteTime = File.GetLastWriteTimeUtc(this.fullPath); if (lastWriteTime > this.fileLastWriteTime) @@ -109,7 +108,7 @@ private async Task ReloadFileAsync(bool isAsync, CancellationToken cancellationT if (simplified?.Entries is not null) { this.overrideValues = simplified.Entries.ToDictionary(kv => kv.Key, kv => kv.Value.ToSetting()); - return; + break; } var deserialized = content.Deserialize() diff --git a/src/ConfigCatClient/Override/OverrideBehaviour.cs b/src/ConfigCatClient/Override/OverrideBehaviour.cs index ab795662..2c515504 100644 --- a/src/ConfigCatClient/Override/OverrideBehaviour.cs +++ b/src/ConfigCatClient/Override/OverrideBehaviour.cs @@ -1,7 +1,7 @@ namespace ConfigCat.Client; /// -/// Describes how the overrides should behave. +/// Specifies the behaviours for flag overrides. /// public enum OverrideBehaviour { diff --git a/src/ConfigCatClient/RefreshResult.cs b/src/ConfigCatClient/RefreshResult.cs index b12fc230..b51fad89 100644 --- a/src/ConfigCatClient/RefreshResult.cs +++ b/src/ConfigCatClient/RefreshResult.cs @@ -9,18 +9,18 @@ namespace ConfigCat.Client; public readonly record struct RefreshResult { /// - /// Creates an instance which indicates that the operation was successful. + /// Creates an instance of the struct which indicates that the operation was successful. /// - /// + /// The new instance. public static RefreshResult Success() { return default; } /// - /// Creates an instance which indicates that the operation failed. + /// Creates an instance of the struct which indicates that the operation failed. /// - /// + /// The new instance. public static RefreshResult Failure(string errorMessage, Exception? errorException = null) { return new RefreshResult(errorMessage ?? throw new ArgumentNullException(nameof(errorMessage)), errorException); diff --git a/src/ConfigCatClient/User.cs b/src/ConfigCatClient/User.cs index e7efef1c..f4b6c920 100644 --- a/src/ConfigCatClient/User.cs +++ b/src/ConfigCatClient/User.cs @@ -8,34 +8,34 @@ namespace ConfigCat.Client; /// -/// Object for variation evaluation +/// User Object. Contains user attributes which are used for evaluating targeting rules and percentage options. /// public class User { internal const string DefaultIdentifierValue = ""; /// - /// Unique identifier for the User or Session. e.g. Email address, Primary key, Session Id + /// The unique identifier of the user or session (e.g. email address, primary key, session ID, etc.) /// public string Identifier { get; private set; } /// - /// Optional parameter for easier targeting rule definitions + /// Email address of the user. /// public string? Email { get; set; } /// - /// Optional parameter for easier targeting rule definitions + /// Country of the user. /// public string? Country { get; set; } /// - /// Optional dictionary for custom attributes of the User for advanced targeting rule definitions. e.g. User role, Subscription type + /// Custom attributes of the user for advanced targeting rule definitions (e.g. user role, subscription type, etc.) /// public IDictionary Custom { get; set; } /// - /// Serve all user attributes + /// Returns all attributes of the user. /// [JsonIgnore] public IReadOnlyDictionary AllAttributes @@ -65,9 +65,9 @@ public class User } /// - /// Create an instance of User + /// Initializes a new instance of the class. /// - /// Unique identifier for the User + /// The unique identifier of the user or session. public User(string identifier) { Identifier = string.IsNullOrEmpty(identifier) ? DefaultIdentifierValue : identifier; diff --git a/src/ConfigCatClient/Utils/DateTimeUtils.cs b/src/ConfigCatClient/Utils/DateTimeUtils.cs index 04f45b7f..8c3096b7 100644 --- a/src/ConfigCatClient/Utils/DateTimeUtils.cs +++ b/src/ConfigCatClient/Utils/DateTimeUtils.cs @@ -22,7 +22,7 @@ public static string ToUnixTimeStamp(this DateTime dateTime) public static bool TryParseUnixTimeStamp(ReadOnlySpan span, out DateTime dateTime) { -#if NET5_0_OR_GREATER || NETSTANDARD2_1 +#if NET5_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER var slice = span; #else var slice = span.ToString(); diff --git a/src/ConfigCatClient/Versioning/SemVersion.cs b/src/ConfigCatClient/Versioning/SemVersion.cs index 22c0a1aa..085b3876 100644 --- a/src/ConfigCatClient/Versioning/SemVersion.cs +++ b/src/ConfigCatClient/Versioning/SemVersion.cs @@ -61,9 +61,9 @@ public sealed class SemVersion : IEquatable, IComparable #if !NETSTANDARD #pragma warning disable CA1801 // Parameter unused /// - /// Deserialize a . + /// Deserializes a . /// - /// The parameter is null. + /// is . private SemVersion(SerializationInfo info, StreamingContext context) #pragma warning restore CA1801 // Parameter unused { @@ -78,7 +78,7 @@ private SemVersion(SerializationInfo info, StreamingContext context) #endif /// - /// Constructs a new instance of the class. + /// Initializes a new instance of the class. /// /// The major version. /// The minor version. @@ -96,7 +96,7 @@ public SemVersion(int major, int minor = 0, int patch = 0, string prerelease = " } /// - /// Constructs a new instance of the class from + /// Initializes a new instance of the class from /// a . /// /// The that is used to initialize @@ -128,9 +128,9 @@ public SemVersion(Version version) /// If set to minor and patch version are required, /// otherwise they are optional. /// The object. - /// The is . - /// The has an invalid format. - /// The is missing Minor or Patch versions and is . + /// is . + /// has an invalid format. + /// is missing Minor or Patch versions and is . /// The Major, Minor, or Patch versions are larger than int.MaxValue. public static SemVersion Parse(string version, bool strict = false) { @@ -235,7 +235,7 @@ public static int Compare(SemVersion versionA, SemVersion versionB) } /// - /// Make a copy of the current instance with changed properties. + /// Creates a copy of the current instance with changed properties. /// /// The value to replace the major version or to leave it unchanged. /// The value to replace the minor version or to leave it unchanged. @@ -345,7 +345,7 @@ public override string ToString() /// Zero: This instance occurs in the same position in the sort order as . /// Greater than zero: This instance follows in the sort order. /// - /// The is not a . + /// is not a . public int CompareTo(object obj) { return CompareTo((SemVersion)obj); @@ -532,7 +532,7 @@ public void GetObjectData(SerializationInfo info, StreamingContext context) ///
/// The semantic version. /// The object. - /// The is . + /// is . /// The version number has an invalid format. /// The Major, Minor, or Patch versions are larger than int.MaxValue. public static implicit operator SemVersion(string version)