├── .github
└── workflows
│ ├── build-and-test.yml
│ └── release.yml
├── .gitignore
├── FeatBit.ServerSdk.sln
├── LICENSE.md
├── README.md
├── examples
├── ConsoleApp
│ ├── ConsoleApp.csproj
│ └── Program.cs
└── WebApiApp
│ ├── FeatBitHealthCheck.cs
│ ├── Program.cs
│ ├── Properties
│ └── launchSettings.json
│ ├── WebApiApp.csproj
│ ├── appsettings.Development.json
│ └── appsettings.json
├── global.json
├── src
└── FeatBit.ServerSdk
│ ├── Bootstrapping
│ ├── IBootstrapProvider.cs
│ ├── JsonBootstrapProvider.cs
│ └── NullBootstrapProvider.cs
│ ├── Concurrent
│ ├── AtomicBoolean.cs
│ └── StatusManager.cs
│ ├── DataSynchronizer
│ ├── DataSet.cs
│ ├── DataSynchronizerStatus.cs
│ ├── IDataSynchronizer.cs
│ ├── NullDataSynchronizer.cs
│ └── WebSocketDataSynchronizer.cs
│ ├── DependencyInjection
│ ├── FbClientHostedService.cs
│ └── ServiceCollectionExtensions.cs
│ ├── Evaluation
│ ├── DispatchAlgorithm.cs
│ ├── EvalDetail.cs
│ ├── EvalResult.cs
│ ├── EvaluationContext.cs
│ ├── Evaluator.ConditionMatcher.cs
│ ├── Evaluator.RuleMatcher.cs
│ ├── Evaluator.SegmentMatcher.cs
│ ├── Evaluator.cs
│ ├── IEvaluator.cs
│ ├── Operator.cs
│ └── OperatorTypes.cs
│ ├── Events
│ ├── DefaultEventBuffer.cs
│ ├── DefaultEventDispatcher.cs
│ ├── DefaultEventDispather.Log.cs
│ ├── DefaultEventProcessor.cs
│ ├── DefaultEventSender.Log.cs
│ ├── DefaultEventSender.cs
│ ├── DefaultEventSerializer.cs
│ ├── DeliveryStatus.cs
│ ├── IEvent.cs
│ ├── IEventBuffer.cs
│ ├── IEventDispatcher.cs
│ ├── IEventProcessor.cs
│ ├── IEventSender.cs
│ ├── IEventSerializer.cs
│ └── NullEventProcessor.cs
│ ├── FbClient.cs
│ ├── FbClientStatus.cs
│ ├── FeatBit.ServerSdk.csproj
│ ├── Http
│ ├── HttpConstants.cs
│ └── HttpErrors.cs
│ ├── IFbClient.cs
│ ├── Json
│ ├── ReusableJsonSerializerOptions.cs
│ └── VersionJsonConverter.cs
│ ├── Model
│ ├── Condition.cs
│ ├── FbUser.cs
│ ├── FbUserBuilder.cs
│ ├── FeatureFlag.cs
│ ├── FeatureFlagBuilder.cs
│ ├── Segment.cs
│ ├── SegmentBuilder.cs
│ └── Variation.cs
│ ├── NuGet.md
│ ├── Options
│ ├── FbOptions.cs
│ └── FbOptionsBuilder.cs
│ ├── Properties
│ └── AssemblyInfo.cs
│ ├── Retry
│ ├── BackoffAndJitterRetryPolicy.cs
│ ├── DefaultRetryPolicy.cs
│ ├── IRetryPolicy.cs
│ └── RetryContext.cs
│ ├── Store
│ ├── DefaultMemoryStore.cs
│ ├── IMemoryStore.cs
│ ├── StorableObject.cs
│ └── StoreKeys.cs
│ ├── Transport
│ ├── ConnectionToken.cs
│ ├── DuplexPipe.cs
│ ├── FbWebSocket.Log.cs
│ ├── FbWebSocket.cs
│ ├── TextMessageFormatter.cs
│ ├── TextMessageParser.cs
│ ├── WebSocketExtensions.cs
│ ├── WebSocketTransport.Log.cs
│ └── WebSocketTransport.cs
│ ├── ValueConverters.cs
│ └── icon.png
└── tests
└── FeatBit.ServerSdk.Tests
├── Bootstrapping
├── JsonBootstrapProviderTests.PopulateStore.verified.txt
├── JsonBootstrapProviderTests.UseValidJson.verified.txt
├── JsonBootstrapProviderTests.cs
└── featbit-bootstrap.json
├── Concurrent
├── AtomicBooleanTests.cs
└── StatusManagerTests.cs
├── DataSynchronizer
├── WebSocketDataSynchronizerTests.cs
├── full-data-set.json
└── patch-data-set.json
├── Evaluation
├── ConditionMatcherTests.cs
├── DispatchAlgorithmTests.cs
├── EvaluatorTests.cs
├── RuleMatcherTests.cs
└── SegmentMatcherTests.cs
├── Events
├── AsyncEventTests.cs
├── DefaultEventBufferTests.cs
├── DefaultEventDispatcherTests.cs
├── DefaultEventProcessorTests.cs
├── DefaultEventSenderTests.cs
├── DefaultEventSerializerTests.SerializeCombinedEvents.verified.txt
├── DefaultEventSerializerTests.SerializeEvalEvent.verified.txt
├── DefaultEventSerializerTests.SerializeEvalEvents.verified.txt
├── DefaultEventSerializerTests.SerializeMetricEvent.verified.txt
├── DefaultEventSerializerTests.SerializeMetricEvents.verified.txt
├── DefaultEventSerializerTests.cs
└── IntEvent.cs
├── FbClientOfflineTests.cs
├── FbClientTests.cs
├── FeatBit.ServerSdk.Tests.csproj
├── Model
├── DeserializationTests.DeserializeFeatureFlag.verified.txt
├── DeserializationTests.DeserializeSegment.verified.txt
├── DeserializationTests.cs
├── one-flag.json
└── one-segment.json
├── Options
├── FbOptionsBuilderTests.HasDefaultValues.verified.txt
└── FbOptionsBuilderTests.cs
├── Retry
├── BackoffAndJitterRetryPolicyTests.cs
└── DefaultRetryPolicyTests.cs
├── Store
└── DefaultMemoryStoreTests.cs
├── TestApp.cs
├── TestAppCollection.cs
├── TestData.cs
├── TestStartup.cs
├── Transport
├── ConnectionTokenTests.cs
├── FbWebSocketTests.cs
└── WebSocketsTransportTests.cs
├── UriTests.cs
├── Usings.cs
└── ValueConverterTests.cs
/.github/workflows/build-and-test.yml:
--------------------------------------------------------------------------------
1 | # This workflow will build a .NET project
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net
3 |
4 | name: build and test
5 |
6 | on:
7 | push:
8 | branches: [ "main" ]
9 | pull_request:
10 | branches: [ "main" ]
11 |
12 | jobs:
13 | build:
14 | runs-on: ubuntu-latest
15 | steps:
16 | - name: Checkout
17 | uses: actions/checkout@v4
18 |
19 | - name: Set up .NET
20 | uses: actions/setup-dotnet@v4
21 | with:
22 | global-json-file: global.json
23 |
24 | - name: Restore Packages
25 | run: dotnet restore
26 |
27 | - name: Build Solution
28 | run: dotnet build -c Release --no-restore
29 |
30 | - name: Run Tests
31 | run: dotnet test -c Release --no-build --verbosity normal
32 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: release
2 |
3 | on:
4 | release:
5 | types:
6 | - published
7 |
8 | env:
9 | VERSION: 1.0.0
10 |
11 | jobs:
12 | build:
13 | environment: Production
14 | runs-on: ubuntu-latest
15 | steps:
16 | - name: Checkout
17 | uses: actions/checkout@v4
18 |
19 | - name: Set up .NET
20 | uses: actions/setup-dotnet@v4
21 | with:
22 | global-json-file: global.json
23 |
24 | - name: Set Version Variable
25 | env:
26 | TAG: ${{ github.ref_name }}
27 | run: echo "VERSION=${TAG#v}" >> $GITHUB_ENV
28 |
29 | - name: Restore Packages
30 | run: dotnet restore
31 |
32 | - name: Build Solution
33 | run: dotnet build -c Release --no-restore /p:Version=$VERSION
34 |
35 | - name: Run Tests
36 | run: dotnet test -c Release --no-build --verbosity normal
37 |
38 | - name: Pack FeatBit.ServerSdk
39 | run: dotnet pack ./src/FeatBit.ServerSdk/FeatBit.ServerSdk.csproj -c Release --no-restore --no-build --output ${VERSION}
40 |
41 | - name: Publish NuGet Package
42 | run: dotnet nuget push ${VERSION}/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate
43 |
--------------------------------------------------------------------------------
/FeatBit.ServerSdk.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.0.31903.59
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{AF2C39EB-C569-437A-863D-8E2ACB81621D}"
7 | EndProject
8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{5AFEEA5E-0264-4EF0-AB21-FB6C1EE46754}"
9 | EndProject
10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FeatBit.ServerSdk", "src\FeatBit.ServerSdk\FeatBit.ServerSdk.csproj", "{BC91B3B1-D996-4F01-9C10-69B05E37A90D}"
11 | EndProject
12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FeatBit.ServerSdk.Tests", "tests\FeatBit.ServerSdk.Tests\FeatBit.ServerSdk.Tests.csproj", "{63F37641-2F11-4B0D-8A88-D345FF03822E}"
13 | EndProject
14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{F6B47580-EB54-432C-AD5A-B918DF4B4C9C}"
15 | EndProject
16 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApp", "examples\ConsoleApp\ConsoleApp.csproj", "{1DC2EE80-678C-4DF1-9298-0465E64B4FDB}"
17 | EndProject
18 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApiApp", "examples\WebApiApp\WebApiApp.csproj", "{68C5EB7D-A4A6-4A86-B38C-C15765F6B883}"
19 | EndProject
20 | Global
21 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
22 | Debug|Any CPU = Debug|Any CPU
23 | Release|Any CPU = Release|Any CPU
24 | EndGlobalSection
25 | GlobalSection(SolutionProperties) = preSolution
26 | HideSolutionNode = FALSE
27 | EndGlobalSection
28 | GlobalSection(NestedProjects) = preSolution
29 | {BC91B3B1-D996-4F01-9C10-69B05E37A90D} = {AF2C39EB-C569-437A-863D-8E2ACB81621D}
30 | {63F37641-2F11-4B0D-8A88-D345FF03822E} = {5AFEEA5E-0264-4EF0-AB21-FB6C1EE46754}
31 | {1DC2EE80-678C-4DF1-9298-0465E64B4FDB} = {F6B47580-EB54-432C-AD5A-B918DF4B4C9C}
32 | {68C5EB7D-A4A6-4A86-B38C-C15765F6B883} = {F6B47580-EB54-432C-AD5A-B918DF4B4C9C}
33 | EndGlobalSection
34 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
35 | {BC91B3B1-D996-4F01-9C10-69B05E37A90D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
36 | {BC91B3B1-D996-4F01-9C10-69B05E37A90D}.Debug|Any CPU.Build.0 = Debug|Any CPU
37 | {BC91B3B1-D996-4F01-9C10-69B05E37A90D}.Release|Any CPU.ActiveCfg = Release|Any CPU
38 | {BC91B3B1-D996-4F01-9C10-69B05E37A90D}.Release|Any CPU.Build.0 = Release|Any CPU
39 | {63F37641-2F11-4B0D-8A88-D345FF03822E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
40 | {63F37641-2F11-4B0D-8A88-D345FF03822E}.Debug|Any CPU.Build.0 = Debug|Any CPU
41 | {63F37641-2F11-4B0D-8A88-D345FF03822E}.Release|Any CPU.ActiveCfg = Release|Any CPU
42 | {63F37641-2F11-4B0D-8A88-D345FF03822E}.Release|Any CPU.Build.0 = Release|Any CPU
43 | {1DC2EE80-678C-4DF1-9298-0465E64B4FDB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
44 | {1DC2EE80-678C-4DF1-9298-0465E64B4FDB}.Debug|Any CPU.Build.0 = Debug|Any CPU
45 | {1DC2EE80-678C-4DF1-9298-0465E64B4FDB}.Release|Any CPU.ActiveCfg = Release|Any CPU
46 | {1DC2EE80-678C-4DF1-9298-0465E64B4FDB}.Release|Any CPU.Build.0 = Release|Any CPU
47 | {68C5EB7D-A4A6-4A86-B38C-C15765F6B883}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
48 | {68C5EB7D-A4A6-4A86-B38C-C15765F6B883}.Debug|Any CPU.Build.0 = Debug|Any CPU
49 | {68C5EB7D-A4A6-4A86-B38C-C15765F6B883}.Release|Any CPU.ActiveCfg = Release|Any CPU
50 | {68C5EB7D-A4A6-4A86-B38C-C15765F6B883}.Release|Any CPU.Build.0 = Release|Any CPU
51 | EndGlobalSection
52 | EndGlobal
53 |
--------------------------------------------------------------------------------
/examples/ConsoleApp/ConsoleApp.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net6.0
6 | enable
7 | enable
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
21 |
22 |
--------------------------------------------------------------------------------
/examples/ConsoleApp/Program.cs:
--------------------------------------------------------------------------------
1 | // See https://aka.ms/new-console-template for more information
2 |
3 | using FeatBit.Sdk.Server;
4 | using FeatBit.Sdk.Server.Model;
5 | using FeatBit.Sdk.Server.Options;
6 | using Microsoft.Extensions.Logging;
7 | using Serilog;
8 |
9 | // Set secret to your FeatBit SDK secret.
10 | const string secret = "";
11 | if (string.IsNullOrWhiteSpace(secret))
12 | {
13 | Console.WriteLine("Please edit Program.cs to set secret to your FeatBit SDK secret first. Exiting...");
14 | Environment.Exit(1);
15 | }
16 |
17 | // Creates a new client to connect to FeatBit with a custom option.
18 |
19 | // init serilog
20 | Log.Logger = new LoggerConfiguration()
21 | // use debug logs when troubleshooting
22 | .MinimumLevel.Debug()
23 | .WriteTo.File("featbit-logs.txt")
24 | .CreateLogger();
25 |
26 | var serilogLoggerFactory = LoggerFactory.Create(opt => opt.AddSerilog());
27 | var options = new FbOptionsBuilder(secret)
28 | .Streaming(new Uri("wss://app-eval.featbit.co"))
29 | .Event(new Uri("https://app-eval.featbit.co"))
30 | .LoggerFactory(serilogLoggerFactory)
31 | .Build();
32 |
33 | var client = new FbClient(options);
34 | if (!client.Initialized)
35 | {
36 | Console.WriteLine("FbClient failed to initialize. Exiting...");
37 | Environment.Exit(-1);
38 | }
39 |
40 | while (true)
41 | {
42 | Console.WriteLine("Please input userKey/flagKey, for example 'user-id/use-new-algorithm'. Input 'exit' to exit.");
43 | var input = Console.ReadLine();
44 | if (string.IsNullOrWhiteSpace(input))
45 | {
46 | continue;
47 | }
48 |
49 | if (input == "exit")
50 | {
51 | Console.WriteLine("Exiting, please wait...");
52 | break;
53 | }
54 |
55 | var keys = input.Split('/');
56 | if (keys.Length != 2)
57 | {
58 | Console.WriteLine();
59 | continue;
60 | }
61 |
62 | var userKey = keys[0];
63 | var flagKey = keys[1];
64 |
65 | var user = FbUser.Builder(userKey).Build();
66 |
67 | var detail = client.StringVariationDetail(flagKey, user, "fallback");
68 | Console.WriteLine($"Kind: {detail.Kind}, Reason: {detail.Reason}, Value: {detail.Value}, ValueId: {detail.ValueId}");
69 | Console.WriteLine();
70 | }
71 |
72 | // Shuts down the client to ensure all pending events are sent.
73 | await client.CloseAsync();
74 |
75 | Environment.Exit(1);
--------------------------------------------------------------------------------
/examples/WebApiApp/FeatBitHealthCheck.cs:
--------------------------------------------------------------------------------
1 | using FeatBit.Sdk.Server;
2 | using Microsoft.Extensions.Diagnostics.HealthChecks;
3 |
4 | namespace WebApiApp;
5 |
6 | public class FeatBitHealthCheck : IHealthCheck
7 | {
8 | private readonly IFbClient _fbClient;
9 |
10 | public FeatBitHealthCheck(IFbClient fbClient)
11 | {
12 | _fbClient = fbClient;
13 | }
14 |
15 | public Task CheckHealthAsync(
16 | HealthCheckContext context,
17 | CancellationToken cancellationToken = default)
18 | {
19 | var status = _fbClient.Status;
20 |
21 | var result = status switch
22 | {
23 | FbClientStatus.Ready => HealthCheckResult.Healthy(),
24 | FbClientStatus.Stale => HealthCheckResult.Degraded(),
25 | _ => HealthCheckResult.Unhealthy()
26 | };
27 |
28 | return Task.FromResult(result);
29 | }
30 | }
--------------------------------------------------------------------------------
/examples/WebApiApp/Program.cs:
--------------------------------------------------------------------------------
1 | using FeatBit.Sdk.Server;
2 | using FeatBit.Sdk.Server.Model;
3 | using FeatBit.Sdk.Server.DependencyInjection;
4 | using HealthChecks.UI.Client;
5 | using Microsoft.AspNetCore.Diagnostics.HealthChecks;
6 | using WebApiApp;
7 |
8 | var builder = WebApplication.CreateBuilder(args);
9 |
10 | // Set secret to your FeatBit SDK secret.
11 | const string secret = "";
12 | if (string.IsNullOrWhiteSpace(secret))
13 | {
14 | Console.WriteLine("Please edit Program.cs to set secret to your FeatBit SDK secret first. Exiting...");
15 | Environment.Exit(1);
16 | }
17 |
18 | // Note that by default, the FeatBit SDK will use the default logger factory provided by ASP.NET Core.
19 | builder.Services.AddFeatBit(options =>
20 | {
21 | options.EnvSecret = secret;
22 | options.StreamingUri = new Uri("wss://app-eval.featbit.co");
23 | options.EventUri = new Uri("https://app-eval.featbit.co");
24 | options.StartWaitTime = TimeSpan.FromSeconds(3);
25 | });
26 |
27 | builder.Services.AddHealthChecks()
28 | .AddCheck("FeatBit");
29 |
30 | var app = builder.Build();
31 |
32 | // curl -X GET --location http://localhost:5014/healthz
33 | app.MapHealthChecks("/healthz", new HealthCheckOptions
34 | {
35 | ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
36 | });
37 |
38 | // curl -X GET --location http://localhost:5014/variation-detail/game-runner?fallbackValue=lol
39 | app.MapGet("/variation-detail/{flagKey}", (IFbClient fbClient, string flagKey, string fallbackValue) =>
40 | {
41 | var user = FbUser.Builder("tester-id").Name("tester").Build();
42 |
43 | return fbClient.StringVariationDetail(flagKey, user, defaultValue: fallbackValue);
44 | });
45 |
46 | app.Run();
--------------------------------------------------------------------------------
/examples/WebApiApp/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/launchsettings.json",
3 | "profiles": {
4 | "WebApiApp": {
5 | "commandName": "Project",
6 | "dotnetRunMessages": true,
7 | "launchBrowser": false,
8 | "applicationUrl": "http://localhost:5014",
9 | "environmentVariables": {
10 | "ASPNETCORE_ENVIRONMENT": "Development"
11 | }
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/examples/WebApiApp/WebApiApp.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | enable
6 | enable
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
19 |
20 |
--------------------------------------------------------------------------------
/examples/WebApiApp/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning",
6 | "FeatBit": "Information"
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/examples/WebApiApp/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning",
6 | "FeatBit": "Information"
7 | }
8 | },
9 | "AllowedHosts": "*"
10 | }
11 |
--------------------------------------------------------------------------------
/global.json:
--------------------------------------------------------------------------------
1 | {
2 | "sdk": {
3 | "version": "6.0.0",
4 | "rollForward": "latestFeature",
5 | "allowPrerelease": false
6 | }
7 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Bootstrapping/IBootstrapProvider.cs:
--------------------------------------------------------------------------------
1 | using FeatBit.Sdk.Server.DataSynchronizer;
2 | using FeatBit.Sdk.Server.Store;
3 |
4 | namespace FeatBit.Sdk.Server.Bootstrapping;
5 |
6 | internal interface IBootstrapProvider
7 | {
8 | DataSet DataSet();
9 |
10 | void Populate(IMemoryStore store);
11 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Bootstrapping/JsonBootstrapProvider.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json;
2 | using FeatBit.Sdk.Server.DataSynchronizer;
3 | using FeatBit.Sdk.Server.Store;
4 |
5 | namespace FeatBit.Sdk.Server.Bootstrapping;
6 |
7 | internal sealed class JsonBootstrapProvider : IBootstrapProvider
8 | {
9 | private readonly DataSet _dataSet;
10 |
11 | public JsonBootstrapProvider(string json)
12 | {
13 | using var jsonDocument = JsonDocument.Parse(json);
14 | var root = jsonDocument.RootElement;
15 | _dataSet = DataSynchronizer.DataSet.FromJsonElement(root.GetProperty("data"));
16 | }
17 |
18 | public DataSet DataSet() => _dataSet;
19 |
20 | public void Populate(IMemoryStore store)
21 | {
22 | var objects = _dataSet.GetStorableObjects();
23 | store.Populate(objects);
24 | }
25 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Bootstrapping/NullBootstrapProvider.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using FeatBit.Sdk.Server.DataSynchronizer;
3 | using FeatBit.Sdk.Server.Model;
4 | using FeatBit.Sdk.Server.Store;
5 |
6 | namespace FeatBit.Sdk.Server.Bootstrapping;
7 |
8 | internal sealed class NullBootstrapProvider : IBootstrapProvider
9 | {
10 | private readonly DataSet _emptySet;
11 |
12 | public NullBootstrapProvider()
13 | {
14 | _emptySet = new DataSet
15 | {
16 | EventType = DataSynchronizer.DataSet.Full,
17 | FeatureFlags = Array.Empty(),
18 | Segments = Array.Empty()
19 | };
20 | }
21 |
22 | public DataSet DataSet() => _emptySet;
23 |
24 | public void Populate(IMemoryStore store)
25 | {
26 | store.Populate(Array.Empty());
27 | }
28 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Concurrent/AtomicBoolean.cs:
--------------------------------------------------------------------------------
1 | using System.Threading;
2 |
3 | // From: https://github.com/akkadotnet/akka.net/blob/dev/src/core/Akka/Util/AtomicBoolean.cs
4 | namespace FeatBit.Sdk.Server.Concurrent
5 | {
6 | ///
7 | /// Implementation of the java.concurrent.util.AtomicBoolean type.
8 | ///
9 | /// Uses internally to enforce ordering of writes
10 | /// without any explicit locking. .NET's strong memory on write guarantees might already enforce
11 | /// this ordering, but the addition of the MemoryBarrier guarantees it.
12 | ///
13 | public sealed class AtomicBoolean
14 | {
15 | private const int FalseValue = 0;
16 | private const int TrueValue = 1;
17 |
18 | private int _value;
19 |
20 | ///
21 | /// Sets the initial value of this to .
22 | ///
23 | /// TBD
24 | public AtomicBoolean(bool initialValue = false)
25 | {
26 | _value = initialValue ? TrueValue : FalseValue;
27 | }
28 |
29 | ///
30 | /// The current value of this
31 | ///
32 | public bool Value
33 | {
34 | get
35 | {
36 | Interlocked.MemoryBarrier();
37 | return _value == TrueValue;
38 | }
39 | set { Interlocked.Exchange(ref _value, value ? TrueValue : FalseValue); }
40 | }
41 |
42 | ///
43 | /// If equals , then set the Value to
44 | /// .
45 | ///
46 | /// TBD
47 | /// TBD
48 | /// true if was set
49 | public bool CompareAndSet(bool expected, bool newValue)
50 | {
51 | var expectedInt = expected ? TrueValue : FalseValue;
52 | var newInt = newValue ? TrueValue : FalseValue;
53 | return Interlocked.CompareExchange(ref _value, newInt, expectedInt) == expectedInt;
54 | }
55 |
56 | ///
57 | /// Atomically sets the to and returns the old .
58 | ///
59 | /// The new value
60 | /// The old value
61 | public bool GetAndSet(bool newValue)
62 | {
63 | return Interlocked.Exchange(ref _value, newValue ? TrueValue : FalseValue) == TrueValue;
64 | }
65 |
66 | ///
67 | /// Performs an implicit conversion from to .
68 | ///
69 | /// The boolean to convert
70 | /// The result of the conversion.
71 | public static implicit operator bool(AtomicBoolean atomicBoolean)
72 | {
73 | return atomicBoolean.Value;
74 | }
75 | }
76 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Concurrent/StatusManager.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace FeatBit.Sdk.Server.Concurrent;
5 |
6 | public sealed class StatusManager where TStatus : Enum
7 | {
8 | private TStatus _status;
9 | private readonly object _statusLock = new object();
10 | private readonly Action _onStatusChanged;
11 |
12 | public StatusManager(TStatus initialStatus, Action onStatusChanged = null)
13 | {
14 | _status = initialStatus;
15 | _onStatusChanged = onStatusChanged;
16 | }
17 |
18 | public TStatus Status
19 | {
20 | get
21 | {
22 | lock (_statusLock)
23 | {
24 | return _status;
25 | }
26 | }
27 | }
28 |
29 | public bool CompareAndSet(TStatus expected, TStatus newStatus)
30 | {
31 | lock (_statusLock)
32 | {
33 | if (!EqualityComparer.Default.Equals(_status, expected))
34 | {
35 | return false;
36 | }
37 |
38 | SetStatus(newStatus);
39 | return true;
40 | }
41 | }
42 |
43 | public void SetStatus(TStatus newStatus)
44 | {
45 | lock (_statusLock)
46 | {
47 | if (EqualityComparer.Default.Equals(_status, newStatus))
48 | {
49 | return;
50 | }
51 |
52 | _status = newStatus;
53 | _onStatusChanged?.Invoke(_status);
54 | }
55 | }
56 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/DataSynchronizer/DataSet.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Text.Json;
3 | using FeatBit.Sdk.Server.Json;
4 | using FeatBit.Sdk.Server.Model;
5 | using FeatBit.Sdk.Server.Store;
6 |
7 | namespace FeatBit.Sdk.Server.DataSynchronizer
8 | {
9 | internal class DataSet
10 | {
11 | public const string Full = "full";
12 | public const string Patch = "patch";
13 |
14 | public string EventType { get; set; }
15 |
16 | public FeatureFlag[] FeatureFlags { get; set; }
17 |
18 | public Segment[] Segments { get; set; }
19 |
20 | internal IEnumerable GetStorableObjects()
21 | {
22 | var objects = new List();
23 | objects.AddRange(FeatureFlags);
24 | objects.AddRange(Segments);
25 |
26 | return objects;
27 | }
28 |
29 | internal static DataSet FromJsonElement(JsonElement jsonElement)
30 | {
31 | #if NETCOREAPP3_1
32 | var rawText = jsonElement.GetRawText();
33 | var dataSet = JsonSerializer.Deserialize(rawText, ReusableJsonSerializerOptions.Web);
34 | #else
35 | // JsonElement.Deserialize only available on .NET 6.0+
36 | var dataSet = jsonElement.Deserialize(ReusableJsonSerializerOptions.Web);
37 | #endif
38 | return dataSet;
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/DataSynchronizer/DataSynchronizerStatus.cs:
--------------------------------------------------------------------------------
1 | namespace FeatBit.Sdk.Server.DataSynchronizer;
2 |
3 | public enum DataSynchronizerStatus
4 | {
5 | ///
6 | /// The initial state of the synchronizer when the SDK is being initialized.
7 | ///
8 | ///
9 | /// If it encounters an error that requires it to retry initialization, the state will remain at
10 | /// until it either succeeds and becomes , or
11 | /// permanently fails and becomes .
12 | ///
13 | Starting,
14 |
15 | ///
16 | /// Indicates that the synchronizer is currently operational and has not had any problems since the
17 | /// last time it received data.
18 | ///
19 | ///
20 | /// In streaming mode, this means that there is currently an open stream connection and that at least
21 | /// one initial message has been received on the stream. In polling mode, it means that the last poll
22 | /// request succeeded.
23 | ///
24 | Stable,
25 |
26 | ///
27 | /// Indicates that the synchronizer encountered an error that it will attempt to recover from.
28 | ///
29 | ///
30 | /// In streaming mode, this means that the stream connection failed, or had to be dropped due to some
31 | /// other error, and will be retried after a backoff delay. In polling mode, it means that the last poll
32 | /// request failed, and a new poll request will be made after the configured polling interval.
33 | ///
34 | Interrupted,
35 |
36 | ///
37 | /// Indicates that the synchronizer has been permanently shut down.
38 | ///
39 | ///
40 | /// This could be because it encountered an unrecoverable error (for instance, the Evaluation server
41 | /// rejected the SDK key: an invalid SDK key will never become valid), or because the SDK client was
42 | /// explicitly shut down.
43 | ///
44 | Stopped
45 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/DataSynchronizer/IDataSynchronizer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 |
4 | namespace FeatBit.Sdk.Server.DataSynchronizer
5 | {
6 | public interface IDataSynchronizer
7 | {
8 | ///
9 | /// Indicates whether the data synchronizer has finished initializing.
10 | ///
11 | public bool Initialized { get; }
12 |
13 | ///
14 | /// The current status of the data synchronizer.
15 | ///
16 | public DataSynchronizerStatus Status { get; }
17 |
18 | /// An event for receiving notifications of status changes.
19 | ///
20 | ///
21 | /// Any handlers attached to this event will be notified whenever any property of the status has changed.
22 | /// See for an explanation of the meaning of each property and what could cause it
23 | /// to change.
24 | ///
25 | ///
26 | /// The listener should return as soon as possible so as not to block subsequent notifications.
27 | ///
28 | ///
29 | event Action StatusChanged;
30 |
31 | ///
32 | /// Starts the data synchronizer. This is called once from the constructor.
33 | ///
34 | /// a Task which is completed once the data synchronizer has finished starting up
35 | Task StartAsync();
36 |
37 | ///
38 | /// Stop the data synchronizer and dispose all resources.
39 | ///
40 | /// The Task
41 | Task StopAsync();
42 | }
43 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/DataSynchronizer/NullDataSynchronizer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using FeatBit.Sdk.Server.Concurrent;
4 |
5 | namespace FeatBit.Sdk.Server.DataSynchronizer;
6 |
7 | internal sealed class NullDataSynchronizer : IDataSynchronizer
8 | {
9 | private readonly StatusManager _statusManager;
10 |
11 | public bool Initialized => true;
12 | public DataSynchronizerStatus Status => _statusManager.Status;
13 | public event Action StatusChanged;
14 |
15 | public NullDataSynchronizer()
16 | {
17 | _statusManager = new StatusManager(
18 | DataSynchronizerStatus.Stable,
19 | OnStatusChanged
20 | );
21 | }
22 |
23 | public Task StartAsync()
24 | {
25 | _statusManager.SetStatus(DataSynchronizerStatus.Stable);
26 | return Task.FromResult(true);
27 | }
28 |
29 | public Task StopAsync()
30 | {
31 | _statusManager.SetStatus(DataSynchronizerStatus.Stopped);
32 | return Task.CompletedTask;
33 | }
34 |
35 | private void OnStatusChanged(DataSynchronizerStatus status) => StatusChanged?.Invoke(status);
36 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/DependencyInjection/FbClientHostedService.cs:
--------------------------------------------------------------------------------
1 | using System.Threading;
2 | using System.Threading.Tasks;
3 | using Microsoft.Extensions.Hosting;
4 |
5 | namespace FeatBit.Sdk.Server.DependencyInjection;
6 |
7 | ///
8 | /// The FbClientHostedService performs the following tasks:
9 | ///
10 | /// -
11 | ///
12 | /// The constructor ensures that the FbClient is created before the application starts.
13 | ///
14 | ///
15 | /// -
16 | ///
17 | /// The method closes the FbClient when the application host is performing a graceful shutdown.
18 | ///
19 | ///
20 | ///
21 | ///
22 | public class FbClientHostedService : IHostedService
23 | {
24 | private readonly IFbClient _fbClient;
25 |
26 | public FbClientHostedService(IFbClient fbClient)
27 | {
28 | _fbClient = fbClient;
29 | }
30 |
31 | public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
32 |
33 | public async Task StopAsync(CancellationToken cancellationToken)
34 | {
35 | await _fbClient.CloseAsync();
36 | }
37 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/DependencyInjection/ServiceCollectionExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using FeatBit.Sdk.Server.Options;
3 | using Microsoft.Extensions.DependencyInjection;
4 | using Microsoft.Extensions.Logging;
5 | using Microsoft.Extensions.Logging.Abstractions;
6 |
7 | namespace FeatBit.Sdk.Server.DependencyInjection;
8 |
9 | public static class ServiceCollectionExtensions
10 | {
11 | ///
12 | /// Adds FeatBit services to the specified .
13 | ///
14 | /// The to add services to.
15 | /// An to configure the provided .
16 | /// This method will block the current thread for a maximum duration specified in .
17 | public static void AddFeatBit(this IServiceCollection services, Action configureOptions)
18 | {
19 | var options = new FbOptionsBuilder().Build();
20 | configureOptions(options);
21 |
22 | AddFeatBit(services, options);
23 | }
24 |
25 | ///
26 | /// Adds FeatBit services to the specified .
27 | ///
28 | /// The to add services to.
29 | /// The options for configuring FeatBit.
30 | /// This method will block the current thread for a maximum duration specified in .
31 | public static void AddFeatBit(this IServiceCollection services, FbOptions options)
32 | {
33 | var serviceDescriptor = new ServiceDescriptor(
34 | typeof(IFbClient),
35 | serviceProvider =>
36 | {
37 | // Configure the logger factory if not provided or set to NullLoggerFactory
38 | if (options.LoggerFactory is null)
39 | {
40 | options.LoggerFactory = NullLoggerFactory.Instance;
41 | }
42 | else if (options.LoggerFactory is NullLoggerFactory)
43 | {
44 | var defaultLoggerFactory = serviceProvider.GetService();
45 | if (defaultLoggerFactory != null)
46 | {
47 | options.LoggerFactory = defaultLoggerFactory;
48 | }
49 | }
50 |
51 | var client = new FbClient(options);
52 | return client;
53 | },
54 | ServiceLifetime.Singleton
55 | );
56 | services.Add(serviceDescriptor);
57 |
58 | // The FbClientHostedService ensures:
59 | // 1. FbClient is created before the application starts.
60 | // 2. FbClient is closed when the application host performs a graceful shutdown.
61 | services.AddHostedService();
62 | }
63 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Evaluation/DispatchAlgorithm.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Security.Cryptography;
3 | using System.Text;
4 |
5 | namespace FeatBit.Sdk.Server.Evaluation
6 | {
7 | internal static class DispatchAlgorithm
8 | {
9 | public static bool IsInRollout(string key, double[] rollouts)
10 | {
11 | var min = rollouts[0];
12 | var max = rollouts[1];
13 |
14 | // if [0, 1]
15 | if (min == 0d && 1d - max < 1e-5)
16 | {
17 | return true;
18 | }
19 |
20 | // if [0, 0]
21 | if (min == 0d && max == 0d)
22 | {
23 | return false;
24 | }
25 |
26 | var rollout = RolloutOfKey(key);
27 | return rollout >= min && rollout <= max;
28 | }
29 |
30 | public static double RolloutOfKey(string key)
31 | {
32 | using (var hasher = MD5.Create())
33 | {
34 | var hashedKey = hasher.ComputeHash(Encoding.UTF8.GetBytes(key));
35 | var magicNumber = BitConverter.ToInt32(hashedKey, 0);
36 | var percentage = Math.Abs((double)magicNumber / int.MinValue);
37 |
38 | return percentage;
39 | }
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Evaluation/EvalDetail.cs:
--------------------------------------------------------------------------------
1 | namespace FeatBit.Sdk.Server.Evaluation
2 | {
3 | public class EvalDetail
4 | {
5 | ///
6 | /// The key of the flag that was evaluated.
7 | ///
8 | public string Key { get; set; }
9 |
10 | ///
11 | /// An enum indicating the category of the reason.
12 | ///
13 | public ReasonKind Kind { get; set; }
14 |
15 | ///
16 | /// A string describing the main factor that influenced the flag evaluation value.
17 | ///
18 | public string Reason { get; set; }
19 |
20 | ///
21 | /// The result of the flag evaluation. This will be either one of the flag's variations or the default
22 | /// value that was specified when the flag was evaluated.
23 | ///
24 | public TValue Value { get; set; }
25 |
26 | ///
27 | /// The id of the flag value that was returned.
28 | ///
29 | public string ValueId { get; set; }
30 |
31 | ///
32 | /// Constructs a new EvalDetail instance.
33 | ///
34 | /// the flag key
35 | /// the reason kind
36 | /// the evaluation reason
37 | /// the flag value
38 | /// the id of flag value
39 | public EvalDetail(string key, ReasonKind kind, string reason, TValue value, string valueId)
40 | {
41 | Key = key;
42 | Kind = kind;
43 | Reason = reason;
44 | Value = value;
45 | ValueId = valueId;
46 | }
47 | }
48 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Evaluation/EvalResult.cs:
--------------------------------------------------------------------------------
1 | using FeatBit.Sdk.Server.Model;
2 |
3 | namespace FeatBit.Sdk.Server.Evaluation
4 | {
5 | internal class EvalResult
6 | {
7 | public ReasonKind Kind { get; set; }
8 |
9 | public string Reason { get; set; }
10 |
11 | public Variation Variation { get; set; }
12 |
13 | private EvalResult(ReasonKind kind, string reason, Variation variation)
14 | {
15 | Kind = kind;
16 | Reason = reason;
17 | Variation = variation;
18 | }
19 |
20 | // Indicates that the caller provided a flag key that did not match any known flag.
21 | public static readonly EvalResult FlagNotFound = new(ReasonKind.Error, "flag not found", Variation.Empty);
22 |
23 | // Indicates that there was an internal inconsistency in the flag data, e.g. a rule specified a nonexistent
24 | // variation.
25 | public static readonly EvalResult MalformedFlag = new(ReasonKind.Error, "malformed flag", Variation.Empty);
26 |
27 | public static EvalResult FlagOff(Variation variation)
28 | {
29 | return new EvalResult(ReasonKind.Off, "flag off", variation);
30 | }
31 |
32 | public static EvalResult Targeted(Variation variation)
33 | {
34 | return new EvalResult(ReasonKind.TargetMatch, "target match", variation);
35 | }
36 |
37 | public static EvalResult RuleMatched(string ruleName, Variation value)
38 | {
39 | return new EvalResult(ReasonKind.RuleMatch, $"match rule {ruleName}", value);
40 | }
41 |
42 | public static EvalResult Fallthrough(Variation variation)
43 | {
44 | return new EvalResult(ReasonKind.Fallthrough, "fall through targets and rules", variation);
45 | }
46 |
47 | public EvalDetail AsEvalDetail(string key)
48 | {
49 | return new EvalDetail(key, Kind, Reason, Variation.Value, Variation.Id);
50 | }
51 | }
52 |
53 | public enum ReasonKind
54 | {
55 | ///
56 | /// Indicates that the caller tried to evaluate a flag before the client had successfully initialized.
57 | ///
58 | ClientNotReady,
59 |
60 | ///
61 | /// Indicates that the flag was off and therefore returned its configured off value.
62 | ///
63 | Off,
64 |
65 | ///
66 | /// Indicates that the flag was on but the user did not match any targets or rules.
67 | ///
68 | Fallthrough,
69 |
70 | ///
71 | /// Indicates that the user key was specifically targeted for this flag.
72 | ///
73 | TargetMatch,
74 |
75 | ///
76 | /// Indicates that the user matched one of the flag's rules.
77 | ///
78 | RuleMatch,
79 |
80 | ///
81 | /// Indicates that the result value was not of the requested type, e.g. you requested a
82 | /// but the value was an .
83 | ///
84 | WrongType,
85 |
86 | ///
87 | /// Indicates that the flag could not be evaluated, e.g. because it does not exist or due to an unexpected
88 | /// error. In this case the result value will be the default value that the caller passed to the client.
89 | ///
90 | Error
91 | }
92 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Evaluation/EvaluationContext.cs:
--------------------------------------------------------------------------------
1 | using FeatBit.Sdk.Server.Model;
2 |
3 | namespace FeatBit.Sdk.Server.Evaluation
4 | {
5 | internal class EvaluationContext
6 | {
7 | public string FlagKey { get; set; }
8 |
9 | public FbUser FbUser { get; set; }
10 | }
11 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Evaluation/Evaluator.ConditionMatcher.cs:
--------------------------------------------------------------------------------
1 | using FeatBit.Sdk.Server.Model;
2 |
3 | namespace FeatBit.Sdk.Server.Evaluation
4 | {
5 | internal partial class Evaluator
6 | {
7 | internal static bool IsMatchCondition(Condition condition, FbUser user)
8 | {
9 | var userValue = user.ValueOf(condition.Property);
10 |
11 | var theOperator = Operator.Get(condition.Op);
12 | return theOperator.IsMatch(userValue, condition.Value);
13 | }
14 | }
15 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Evaluation/Evaluator.RuleMatcher.cs:
--------------------------------------------------------------------------------
1 | using FeatBit.Sdk.Server.Model;
2 |
3 | namespace FeatBit.Sdk.Server.Evaluation
4 | {
5 | internal partial class Evaluator
6 | {
7 | internal const string IsInSegmentProperty = "User is in segment";
8 |
9 | internal const string IsNotInSegmentProperty = "User is not in segment";
10 |
11 | internal bool IsMatchRule(TargetRule rule, FbUser user)
12 | {
13 | foreach (var condition in rule.Conditions)
14 | {
15 | // in segment condition
16 | if (condition.Property is IsInSegmentProperty)
17 | {
18 | if (!IsMatchAnySegment(condition, user))
19 | {
20 | return false;
21 | }
22 | }
23 | // not in segment condition
24 | else if (condition.Property is IsNotInSegmentProperty)
25 | {
26 | if (IsMatchAnySegment(condition, user))
27 | {
28 | return false;
29 | }
30 | }
31 | // common condition
32 | else if (!IsMatchCondition(condition, user))
33 | {
34 | return false;
35 | }
36 | }
37 |
38 | return true;
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Evaluation/Evaluator.SegmentMatcher.cs:
--------------------------------------------------------------------------------
1 | using System.Linq;
2 | using System.Text.Json;
3 | using FeatBit.Sdk.Server.Model;
4 | using FeatBit.Sdk.Server.Store;
5 |
6 | namespace FeatBit.Sdk.Server.Evaluation
7 | {
8 | internal partial class Evaluator
9 | {
10 | internal static bool IsMatchSegment(Segment segment, FbUser user)
11 | {
12 | if (segment.Excluded.Contains(user.Key))
13 | {
14 | return false;
15 | }
16 |
17 | if (segment.Included.Contains(user.Key))
18 | {
19 | return true;
20 | }
21 |
22 | // if any rule match this user
23 | return segment.Rules.Any(
24 | rule => rule.Conditions.All(condition => IsMatchCondition(condition, user))
25 | );
26 | }
27 |
28 | internal bool IsMatchAnySegment(Condition segmentCondition, FbUser user)
29 | {
30 | var segmentIds = JsonSerializer.Deserialize(segmentCondition.Value);
31 | if (segmentIds == null || !segmentIds.Any())
32 | {
33 | return false;
34 | }
35 |
36 | foreach (var segmentId in segmentIds)
37 | {
38 | var segment = _store.Get(StoreKeys.ForSegment(segmentId));
39 | if (IsMatchSegment(segment, user))
40 | {
41 | return true;
42 | }
43 | }
44 |
45 | return false;
46 | }
47 | }
48 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Evaluation/IEvaluator.cs:
--------------------------------------------------------------------------------
1 | using FeatBit.Sdk.Server.Events;
2 | using FeatBit.Sdk.Server.Model;
3 |
4 | namespace FeatBit.Sdk.Server.Evaluation
5 | {
6 | internal interface IEvaluator
7 | {
8 | (EvalResult evalResult, EvalEvent evalEvent) Evaluate(EvaluationContext context);
9 |
10 | (EvalResult evalResult, EvalEvent evalEvent) Evaluate(FeatureFlag flag, FbUser user);
11 | }
12 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Evaluation/OperatorTypes.cs:
--------------------------------------------------------------------------------
1 | namespace FeatBit.Sdk.Server.Evaluation
2 | {
3 | public class OperatorTypes
4 | {
5 | // numeric
6 | public const string LessThan = "LessThan";
7 | public const string BiggerThan = "BiggerThan";
8 | public const string LessEqualThan = "LessEqualThan";
9 | public const string BiggerEqualThan = "BiggerEqualThan";
10 |
11 | // compare
12 | public const string Equal = "Equal";
13 | public const string NotEqual = "NotEqual";
14 |
15 | // contains/not contains
16 | public const string Contains = "Contains";
17 | public const string NotContain = "NotContain";
18 |
19 | // starts with/ends with
20 | public const string StartsWith = "StartsWith";
21 | public const string EndsWith = "EndsWith";
22 |
23 | // match regex/not match regex
24 | public const string MatchRegex = "MatchRegex";
25 | public const string NotMatchRegex = "NotMatchRegex";
26 |
27 | // is one of/ not one of
28 | public const string IsOneOf = "IsOneOf";
29 | public const string NotOneOf = "NotOneOf";
30 |
31 | // is true/ is false
32 | public const string IsTrue = "IsTrue";
33 | public const string IsFalse = "IsFalse";
34 | }
35 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Events/DefaultEventBuffer.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace FeatBit.Sdk.Server.Events
4 | {
5 | internal sealed class DefaultEventBuffer : IEventBuffer
6 | {
7 | private readonly int _capacity;
8 | private readonly List _events;
9 |
10 | public DefaultEventBuffer(int capacity)
11 | {
12 | _capacity = capacity;
13 | _events = new List();
14 | }
15 |
16 | public bool AddEvent(IEvent @event)
17 | {
18 | if (_events.Count >= _capacity)
19 | {
20 | return false;
21 | }
22 |
23 | _events.Add(@event);
24 | return true;
25 | }
26 |
27 | public int Count => _events.Count;
28 |
29 | public bool IsEmpty => Count == 0;
30 |
31 | public void Clear() => _events.Clear();
32 |
33 | public IEvent[] EventsSnapshot => _events.ToArray();
34 | }
35 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Events/DefaultEventDispather.Log.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.Extensions.Logging;
3 |
4 | namespace FeatBit.Sdk.Server.Events;
5 |
6 | internal sealed partial class DefaultEventDispatcher
7 | {
8 | private static partial class Log
9 | {
10 | [LoggerMessage(1, LogLevel.Debug, "Start dispatch loop.")]
11 | public static partial void StartDispatchLoop(ILogger logger);
12 |
13 | [LoggerMessage(2, LogLevel.Debug, "Finish dispatch loop.")]
14 | public static partial void FinishDispatchLoop(ILogger logger);
15 |
16 | [LoggerMessage(3, LogLevel.Debug, "Added event to buffer.")]
17 | public static partial void AddedEventToBuffer(ILogger logger);
18 |
19 | [LoggerMessage(4, LogLevel.Debug, "{Count} events has been flushed.")]
20 | public static partial void EventsFlushed(ILogger logger, int count);
21 |
22 | [LoggerMessage(5, LogLevel.Debug, "Flush empty buffer.")]
23 | public static partial void FlushEmptyBuffer(ILogger logger);
24 |
25 | [LoggerMessage(6, LogLevel.Warning,
26 | "Exceeded event queue capacity, event will be dropped. Increase capacity to avoid dropping events.")]
27 | public static partial void ExceededCapacity(ILogger logger);
28 |
29 | [LoggerMessage(7, LogLevel.Debug,
30 | "The number of flush workers has reached the limit. This flush event will be skipped.")]
31 | public static partial void TooManyFlushWorkers(ILogger logger);
32 |
33 | [LoggerMessage(8, LogLevel.Error, "Unexpected error in event dispatcher thread.")]
34 | public static partial void DispatchError(ILogger logger, Exception ex);
35 | }
36 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Events/DefaultEventProcessor.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Concurrent;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 | using FeatBit.Sdk.Server.Concurrent;
6 | using FeatBit.Sdk.Server.Options;
7 | using Microsoft.Extensions.Logging;
8 |
9 | namespace FeatBit.Sdk.Server.Events
10 | {
11 | internal sealed class DefaultEventProcessor : IEventProcessor
12 | {
13 | private readonly BlockingCollection _eventQueue;
14 | private readonly Timer _flushTimer;
15 | private readonly IEventDispatcher _eventDispatcher;
16 | private readonly ILogger _logger;
17 |
18 | private readonly AtomicBoolean _closed = new AtomicBoolean();
19 | internal bool HasClosed => _closed.Value; // internal for testing
20 | private readonly AtomicBoolean _capacityExceeded = new AtomicBoolean();
21 |
22 | public DefaultEventProcessor(
23 | FbOptions options,
24 | ILogger logger = null,
25 | Func, IEventDispatcher> dispatcherFactory = null)
26 | {
27 | _eventQueue = new BlockingCollection(options.MaxEventsInQueue);
28 | _flushTimer = new Timer(AutoFlush, null, options.AutoFlushInterval, options.AutoFlushInterval);
29 |
30 | var factory = dispatcherFactory ?? DefaultEventDispatcherFactory;
31 | _eventDispatcher = factory(options, _eventQueue);
32 |
33 | _logger = logger ?? options.LoggerFactory.CreateLogger();
34 | }
35 |
36 | private static IEventDispatcher DefaultEventDispatcherFactory(FbOptions options, BlockingCollection queue)
37 | {
38 | return new DefaultEventDispatcher(options, queue);
39 | }
40 |
41 | public bool Record(IEvent @event)
42 | {
43 | if (@event == null)
44 | {
45 | return false;
46 | }
47 |
48 | try
49 | {
50 | if (_eventQueue.TryAdd(@event))
51 | {
52 | _capacityExceeded.GetAndSet(false);
53 | }
54 | else
55 | {
56 | if (!_capacityExceeded.GetAndSet(true))
57 | {
58 | // The main thread is seriously backed up with not-yet-processed events.
59 | _logger.LogWarning(
60 | "Events are being produced faster than they can be processed. We shouldn't see this."
61 | );
62 | }
63 |
64 | // If the message is a flush message, then it could never be completed if we cannot
65 | // add it to the queue. So we are going to complete it here to prevent the calling
66 | // code from hanging indefinitely.
67 | if (@event is FlushEvent flushEvent)
68 | {
69 | flushEvent.Complete();
70 | }
71 |
72 | return false;
73 | }
74 | }
75 | catch (Exception ex)
76 | {
77 | _logger.LogError(ex, "Error adding event in a queue.");
78 | return false;
79 | }
80 |
81 | return true;
82 | }
83 |
84 | public void Flush()
85 | {
86 | Record(new FlushEvent());
87 | }
88 |
89 | public bool FlushAndWait(TimeSpan timeout)
90 | {
91 | var flush = new FlushEvent();
92 | Record(flush);
93 | return flush.WaitForCompletion(timeout);
94 | }
95 |
96 | public async Task FlushAndWaitAsync(TimeSpan timeout)
97 | {
98 | var flush = new FlushEvent();
99 | Record(flush);
100 | return await flush.WaitForCompletionAsync(timeout);
101 | }
102 |
103 | public void FlushAndClose(TimeSpan timeout)
104 | {
105 | if (_closed.GetAndSet(true))
106 | {
107 | // already closed, nothing more to do
108 | return;
109 | }
110 |
111 | // stop flush timer if it was running
112 | _flushTimer?.Dispose();
113 |
114 | // flush remaining events
115 | Record(new FlushEvent());
116 |
117 | // send an shutdown event to dispatcher
118 | var shutdown = new ShutdownEvent();
119 | Record(shutdown);
120 |
121 | // wait for the shutdown event to complete within the specified timeout
122 | var successfullyShutdown = shutdown.WaitForCompletion(timeout);
123 | if (!successfullyShutdown)
124 | {
125 | _logger.LogWarning("Event processor shutdown did not complete within the specified timeout.");
126 | }
127 |
128 | // mark the event queue as complete for adding
129 | _eventQueue.CompleteAdding();
130 |
131 | // dispose resources
132 | _eventDispatcher?.Dispose();
133 | _eventQueue.Dispose();
134 | }
135 |
136 | private void AutoFlush(object stateInfo)
137 | {
138 | Flush();
139 | }
140 | }
141 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Events/DefaultEventSender.Log.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.Extensions.Logging;
3 |
4 | namespace FeatBit.Sdk.Server.Events
5 | {
6 | internal partial class DefaultEventSender
7 | {
8 | private static partial class Log
9 | {
10 | [LoggerMessage(1, LogLevel.Debug, "Start send event: {Body}")]
11 | public static partial void SendStarted(ILogger logger, string body);
12 |
13 | [LoggerMessage(2, LogLevel.Debug, "Event delivery took {ElapsedMs} ms, response status {Status}.")]
14 | public static partial void SendFinished(ILogger logger, long elapsedMs, int status);
15 |
16 | [LoggerMessage(3, LogLevel.Debug, "Event sending task was cancelled due to a handle timeout.")]
17 | public static partial void SendTaskWasCanceled(ILogger logger);
18 |
19 | [LoggerMessage(4, LogLevel.Debug, "Exception occurred when sending event.")]
20 | public static partial void ErrorSendEvent(ILogger logger, Exception ex);
21 |
22 | [LoggerMessage(5, LogLevel.Warning, "Send event failed: {Reason}.")]
23 | public static partial void SendFailed(ILogger logger, string reason);
24 | }
25 | }
26 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Events/DefaultEventSender.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using System.Net.Http;
4 | using System.Net.Http.Headers;
5 | using System.Text;
6 | using System.Threading;
7 | using System.Threading.Tasks;
8 | using FeatBit.Sdk.Server.Http;
9 | using FeatBit.Sdk.Server.Options;
10 | using Microsoft.Extensions.Logging;
11 |
12 | namespace FeatBit.Sdk.Server.Events
13 | {
14 | internal partial class DefaultEventSender : IEventSender
15 | {
16 | private static readonly TimeSpan DefaultConnectTimeout = TimeSpan.FromSeconds(1);
17 | private static readonly TimeSpan DefaultReadTimeout = TimeSpan.FromSeconds(1);
18 | private static readonly TimeSpan DefaultTimeout = DefaultConnectTimeout + DefaultReadTimeout;
19 |
20 | private readonly Uri _eventUri;
21 | private const string EventPath = "api/public/insight/track";
22 | private readonly int _maxAttempts;
23 | private readonly TimeSpan _retryInterval;
24 |
25 | private readonly HttpClient _httpClient;
26 |
27 | private static readonly MediaTypeHeaderValue JsonContentType = new MediaTypeHeaderValue("application/json")
28 | {
29 | CharSet = "utf-8"
30 | };
31 |
32 | private readonly Stopwatch _stopwatch = new Stopwatch();
33 | private readonly ILogger _logger;
34 |
35 | public DefaultEventSender(FbOptions options, HttpClient httpClient = null)
36 | {
37 | _httpClient = httpClient ?? NewHttpClient();
38 | AddDefaultHeaders(options);
39 |
40 | _eventUri = new Uri(options.EventUri, EventPath);
41 | _maxAttempts = options.MaxSendEventAttempts;
42 | _retryInterval = options.SendEventRetryInterval;
43 |
44 | _logger = options.LoggerFactory.CreateLogger();
45 | }
46 |
47 | public async Task SendAsync(byte[] payload)
48 | {
49 | for (var attempt = 0; attempt < _maxAttempts; attempt++)
50 | {
51 | if (attempt > 0)
52 | {
53 | await Task.Delay(_retryInterval);
54 | }
55 |
56 | bool isRecoverable;
57 | string error;
58 | using var cts = new CancellationTokenSource(DefaultTimeout);
59 |
60 | try
61 | {
62 | var response = await SendCoreAsync(payload, cts);
63 | if (response.IsSuccessStatusCode)
64 | {
65 | return DeliveryStatus.Succeeded;
66 | }
67 |
68 | error = response.ReasonPhrase;
69 | isRecoverable = HttpErrors.IsRecoverable((int)response.StatusCode);
70 | }
71 | catch (TaskCanceledException ex)
72 | {
73 | if (ex.CancellationToken == cts.Token)
74 | {
75 | Log.SendTaskWasCanceled(_logger);
76 |
77 | // The task was canceled due to a handle timeout, do not retry it.
78 | return DeliveryStatus.Failed;
79 | }
80 |
81 | // Otherwise this was a request timeout.
82 | isRecoverable = true;
83 | error = "request timeout";
84 | }
85 | catch (Exception ex)
86 | {
87 | Log.ErrorSendEvent(_logger, ex);
88 | isRecoverable = true;
89 | error = ex.Message;
90 | }
91 |
92 | Log.SendFailed(_logger, error);
93 | if (!isRecoverable)
94 | {
95 | return DeliveryStatus.FailedAndMustShutDown;
96 | }
97 | }
98 |
99 | Log.SendFailed(_logger, "Reconnect retries have been exhausted after max failed attempts.");
100 | return DeliveryStatus.Failed;
101 | }
102 |
103 | private async Task SendCoreAsync(byte[] payload, CancellationTokenSource cts)
104 | {
105 | // check log level to avoid unnecessary string allocation
106 | if (_logger.IsEnabled(LogLevel.Trace))
107 | {
108 | var body = Encoding.UTF8.GetString(payload);
109 | Log.SendStarted(_logger, body);
110 | }
111 |
112 | using var content = new ByteArrayContent(payload);
113 | content.Headers.ContentType = JsonContentType;
114 |
115 | _stopwatch.Restart();
116 | using var response = await _httpClient.PostAsync(_eventUri, content, cts.Token);
117 | _stopwatch.Stop();
118 |
119 | Log.SendFinished(_logger, _stopwatch.ElapsedMilliseconds, (int)response.StatusCode);
120 |
121 | return response;
122 | }
123 |
124 | private static HttpClient NewHttpClient()
125 | {
126 | #if NETCOREAPP || NET6_0
127 | var handler = new SocketsHttpHandler
128 | {
129 | ConnectTimeout = DefaultConnectTimeout
130 | };
131 | #else
132 | var handler = new HttpClientHandler();
133 | #endif
134 | var client = new HttpClient(handler, false);
135 | return client;
136 | }
137 |
138 | private void AddDefaultHeaders(FbOptions options)
139 | {
140 | _httpClient.DefaultRequestHeaders.Add("Authorization", options.EnvSecret);
141 | _httpClient.DefaultRequestHeaders.Add("User-Agent", HttpConstants.UserAgent);
142 | }
143 | }
144 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Events/DefaultEventSerializer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Text.Json;
4 | using FeatBit.Sdk.Server.Model;
5 |
6 | namespace FeatBit.Sdk.Server.Events
7 | {
8 | internal class DefaultEventSerializer : IEventSerializer
9 | {
10 | public byte[] Serialize(IEvent @event)
11 | {
12 | using var stream = new MemoryStream();
13 | using var writer = new Utf8JsonWriter(stream);
14 |
15 | WriteEvent(@event, writer);
16 |
17 | writer.Flush();
18 | return stream.ToArray();
19 | }
20 |
21 | public byte[] Serialize(ReadOnlyMemory events)
22 | {
23 | var span = events.Span;
24 |
25 | using var stream = new MemoryStream();
26 | using var writer = new Utf8JsonWriter(stream);
27 |
28 | writer.WriteStartArray();
29 | for (var i = 0; i < span.Length; i++)
30 | {
31 | WriteEvent(span[i], writer);
32 | }
33 |
34 | writer.WriteEndArray();
35 |
36 | writer.Flush();
37 | return stream.ToArray();
38 | }
39 |
40 | private static void WriteEvent(IEvent ue, Utf8JsonWriter writer)
41 | {
42 | switch (ue)
43 | {
44 | case EvalEvent ee:
45 | WriteEvalEvent(ee, writer);
46 | break;
47 | case MetricEvent me:
48 | WriteMetricEvent(me, writer);
49 | break;
50 | }
51 | }
52 |
53 | private static void WriteEvalEvent(EvalEvent ee, Utf8JsonWriter writer)
54 | {
55 | writer.WriteStartObject();
56 |
57 | WriteUser(ee.User, writer);
58 |
59 | writer.WriteStartArray("variations");
60 |
61 | writer.WriteStartObject();
62 | writer.WriteString("featureFlagKey", ee.FlagKey);
63 | WriteVariation(ee.Variation, writer);
64 | writer.WriteNumber("timestamp", ee.Timestamp);
65 | writer.WriteBoolean("sendToExperiment", ee.SendToExperiment);
66 | writer.WriteEndObject();
67 |
68 | writer.WriteEndArray();
69 |
70 | writer.WriteEndObject();
71 | }
72 |
73 | private static void WriteVariation(Variation variation, Utf8JsonWriter writer)
74 | {
75 | writer.WriteStartObject("variation");
76 |
77 | writer.WriteString("id", variation.Id);
78 | writer.WriteString("value", variation.Value);
79 |
80 | writer.WriteEndObject();
81 | }
82 |
83 | private static void WriteUser(FbUser user, Utf8JsonWriter writer)
84 | {
85 | writer.WriteStartObject("user");
86 |
87 | writer.WriteString("keyId", user.Key);
88 | writer.WriteString("name", user.Name);
89 |
90 | writer.WriteStartArray("customizedProperties");
91 | foreach (var kv in user.Custom)
92 | {
93 | writer.WriteStartObject();
94 | writer.WriteString("name", kv.Key);
95 | writer.WriteString("value", kv.Value);
96 | writer.WriteEndObject();
97 | }
98 |
99 | writer.WriteEndArray();
100 |
101 | writer.WriteEndObject();
102 | }
103 |
104 | private static void WriteMetricEvent(MetricEvent ee, Utf8JsonWriter writer)
105 | {
106 | writer.WriteStartObject();
107 |
108 | WriteUser(ee.User, writer);
109 |
110 | writer.WriteStartArray("metrics");
111 |
112 | writer.WriteStartObject();
113 | writer.WriteString("appType", MetricEvent.AppType);
114 | writer.WriteString("route", MetricEvent.Route);
115 | writer.WriteString("type", MetricEvent.Type);
116 | writer.WriteString("eventName", ee.EventName);
117 | writer.WriteNumber("numericValue", ee.NumericValue);
118 | writer.WriteNumber("timestamp", ee.Timestamp);
119 | writer.WriteEndObject();
120 |
121 | writer.WriteEndArray();
122 |
123 | writer.WriteEndObject();
124 | }
125 | }
126 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Events/DeliveryStatus.cs:
--------------------------------------------------------------------------------
1 | namespace FeatBit.Sdk.Server.Events
2 | {
3 | internal enum DeliveryStatus
4 | {
5 | Succeeded,
6 | Failed,
7 | FailedAndMustShutDown
8 | }
9 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Events/IEvent.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using FeatBit.Sdk.Server.Model;
4 |
5 | namespace FeatBit.Sdk.Server.Events
6 | {
7 | internal interface IEvent
8 | {
9 | }
10 |
11 | internal abstract class AsyncEvent : IEvent
12 | {
13 | private readonly TaskCompletionSource _innerTcs;
14 | private readonly Task _innerTask;
15 |
16 | public bool IsCompleted => _innerTask.IsCompleted;
17 |
18 | internal AsyncEvent()
19 | {
20 | _innerTcs = new TaskCompletionSource();
21 | _innerTask = _innerTcs.Task;
22 | }
23 |
24 | internal bool WaitForCompletion(TimeSpan timeout)
25 | {
26 | if (timeout <= TimeSpan.Zero)
27 | {
28 | _innerTask.Wait();
29 | return true;
30 | }
31 |
32 | return _innerTask.Wait(timeout);
33 | }
34 |
35 | internal Task WaitForCompletionAsync(TimeSpan timeout)
36 | {
37 | if (timeout <= TimeSpan.Zero)
38 | {
39 | return _innerTask;
40 | }
41 |
42 | var timeoutTask = Task.Delay(timeout).ContinueWith(_ => false);
43 | return Task.WhenAny(_innerTask, timeoutTask).Result;
44 | }
45 |
46 | internal void Complete()
47 | {
48 | _innerTcs.SetResult(true);
49 | }
50 | }
51 |
52 | internal sealed class FlushEvent : AsyncEvent
53 | {
54 | }
55 |
56 | internal sealed class ShutdownEvent : AsyncEvent
57 | {
58 | }
59 |
60 | internal class PayloadEvent : IEvent
61 | {
62 | }
63 |
64 | internal sealed class EvalEvent : PayloadEvent
65 | {
66 | public FbUser User { get; set; }
67 |
68 | public string FlagKey { get; set; }
69 |
70 | public long Timestamp { get; set; }
71 |
72 | public Variation Variation { get; set; }
73 |
74 | public bool SendToExperiment { get; set; }
75 |
76 | public EvalEvent(FbUser user, string flagKey, Variation variation, bool sendToExperiment)
77 | {
78 | User = user;
79 | FlagKey = flagKey;
80 | Variation = variation;
81 | Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
82 | SendToExperiment = sendToExperiment;
83 | }
84 | }
85 |
86 | internal sealed class MetricEvent : PayloadEvent
87 | {
88 | public const string AppType = "dotnet-server-side";
89 | public const string Route = "index/metric";
90 | public const string Type = "CustomEvent";
91 |
92 | public FbUser User { get; set; }
93 |
94 | public string EventName { get; set; }
95 |
96 | public double NumericValue { get; set; }
97 |
98 | public long Timestamp { get; set; }
99 |
100 | public MetricEvent(FbUser user, string eventName, double numericValue)
101 | {
102 | User = user;
103 | EventName = eventName;
104 | NumericValue = numericValue;
105 | Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
106 | }
107 | }
108 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Events/IEventBuffer.cs:
--------------------------------------------------------------------------------
1 | namespace FeatBit.Sdk.Server.Events
2 | {
3 | internal interface IEventBuffer
4 | {
5 | bool AddEvent(IEvent @event);
6 |
7 | bool IsEmpty { get; }
8 |
9 | void Clear();
10 |
11 | IEvent[] EventsSnapshot { get; }
12 | }
13 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Events/IEventDispatcher.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace FeatBit.Sdk.Server.Events
4 | {
5 | internal interface IEventDispatcher : IDisposable
6 | {
7 | }
8 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Events/IEventProcessor.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 |
4 | namespace FeatBit.Sdk.Server.Events
5 | {
6 | ///
7 | /// Represents a processor that can process events.
8 | ///
9 | internal interface IEventProcessor
10 | {
11 | ///
12 | /// Records an .
13 | ///
14 | /// The event to be recorded.
15 | /// A boolean value indicating whether the operation succeeded or not.
16 | bool Record(IEvent @event);
17 |
18 | ///
19 | /// Triggers an asynchronous event flush.
20 | ///
21 | void Flush();
22 |
23 | ///
24 | /// Blocking version of .
25 | ///
26 | /// maximum time to wait; zero or negative timeout means indefinitely
27 | /// true if completed, false if timed out
28 | bool FlushAndWait(TimeSpan timeout);
29 |
30 | ///
31 | /// Asynchronous version of .
32 | ///
33 | ///
34 | /// The difference between this and is that you can await the task to simulate
35 | /// blocking behavior.
36 | ///
37 | /// maximum time to wait; zero or negative timeout means indefinitely
38 | /// a task that resolves to true if completed, false if timed out
39 | Task FlushAndWaitAsync(TimeSpan timeout);
40 |
41 | ///
42 | /// Flush all events and close this processor.
43 | ///
44 | /// maximum time to wait; zero or negative timeout means indefinitely
45 | /// true if completed, false if timed out
46 | void FlushAndClose(TimeSpan timeout);
47 | }
48 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Events/IEventSender.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 |
3 | namespace FeatBit.Sdk.Server.Events
4 | {
5 | internal interface IEventSender
6 | {
7 | Task SendAsync(byte[] payload);
8 | }
9 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Events/IEventSerializer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace FeatBit.Sdk.Server.Events
4 | {
5 | internal interface IEventSerializer
6 | {
7 | public byte[] Serialize(IEvent @event);
8 |
9 | public byte[] Serialize(ReadOnlyMemory events);
10 | }
11 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Events/NullEventProcessor.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 |
4 | namespace FeatBit.Sdk.Server.Events;
5 |
6 | internal sealed class NullEventProcessor : IEventProcessor
7 | {
8 | public bool Record(IEvent @event) => true;
9 |
10 | public void Flush()
11 | {
12 | }
13 |
14 | public bool FlushAndWait(TimeSpan timeout)
15 | {
16 | return true;
17 | }
18 |
19 | public Task FlushAndWaitAsync(TimeSpan timeout)
20 | {
21 | return Task.FromResult(true);
22 | }
23 |
24 | public void FlushAndClose(TimeSpan timeout)
25 | {
26 | }
27 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/FbClientStatus.cs:
--------------------------------------------------------------------------------
1 | namespace FeatBit.Sdk.Server;
2 |
3 | public enum FbClientStatus
4 | {
5 | ///
6 | /// FbClient has not been initialized and cannot yet evaluate flags.
7 | ///
8 | NotReady,
9 |
10 | ///
11 | /// FbClient is ready to resolve flags.
12 | ///
13 | Ready,
14 |
15 | ///
16 | /// FbClient's cached data may not be up-to-date with the source of truth.
17 | ///
18 | Stale,
19 |
20 | ///
21 | /// FbClient has entered an irrecoverable error state or has been explicitly shut down.
22 | ///
23 | Closed
24 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/FeatBit.ServerSdk.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0;netstandard2.1;netcoreapp3.1;net6.0;net462
5 | portable
6 | FeatBit.ServerSdk
7 | Library
8 | FeatBit.ServerSdk
9 | FeatBit.Sdk.Server
10 | FeatBit Server-Side .NET SDK
11 | FeatBit
12 | FeatBit
13 | FeatBit
14 | Copyright 2023 FeatBit
15 | true
16 | snupkg
17 | Apache-2.0
18 | https://github.com/featbit/featbit-dotnet-sdk
19 | https://github.com/featbit/featbit-dotnet-sdk
20 | main
21 | icon.png
22 | git
23 | NuGet.md
24 | true
25 | featbit feature toggle featuretoggle continuous delivery featuremanagement feature-flags toggling
26 | true
27 | 1591;
28 | 10
29 |
30 |
31 |
32 | true
33 | true
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Http/HttpConstants.cs:
--------------------------------------------------------------------------------
1 | namespace FeatBit.Sdk.Server.Http;
2 |
3 | public static class HttpConstants
4 | {
5 | public const string UserAgent = "featbit-dotnet-server-sdk";
6 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Http/HttpErrors.cs:
--------------------------------------------------------------------------------
1 | namespace FeatBit.Sdk.Server.Http;
2 |
3 | internal static class HttpErrors
4 | {
5 | ///
6 | /// Returns true if this type of error could be expected to eventually resolve itself,
7 | /// or false if it indicates a configuration problem or client logic error such that the
8 | /// client should give up on making any further requests.
9 | ///
10 | /// a status code
11 | /// true if retrying is appropriate
12 | public static bool IsRecoverable(int status)
13 | {
14 | if (status is >= 400 and <= 499)
15 | {
16 | return status is 400 or 408 or 429;
17 | }
18 |
19 | return true;
20 | }
21 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Json/ReusableJsonSerializerOptions.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json;
2 |
3 | namespace FeatBit.Sdk.Server.Json
4 | {
5 | public class ReusableJsonSerializerOptions
6 | {
7 | #if NETCOREAPP3_1
8 | // https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/configure-options?pivots=dotnet-core-3-1
9 | // A JsonSerializerOptions constructor that specifies a set of defaults is not available in .NET Core 3.1.
10 | public static readonly JsonSerializerOptions Web = new JsonSerializerOptions
11 | {
12 | PropertyNameCaseInsensitive = true,
13 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase
14 | };
15 | #else
16 | // https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/configure-options?pivots=dotnet-6-0#web-defaults-for-jsonserializeroptions
17 | public static readonly JsonSerializerOptions Web = new JsonSerializerOptions(JsonSerializerDefaults.Web);
18 | #endif
19 | }
20 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Json/VersionJsonConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text.Json;
3 | using System.Text.Json.Serialization;
4 |
5 | namespace FeatBit.Sdk.Server.Json
6 | {
7 | ///
8 | /// Convert updatedAt field to version field.
9 | ///
10 | internal class VersionJsonConverter : JsonConverter
11 | {
12 | public override long Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
13 | {
14 | return reader.GetDateTimeOffset().ToUnixTimeMilliseconds();
15 | }
16 |
17 | public override void Write(Utf8JsonWriter writer, long value, JsonSerializerOptions options)
18 | {
19 | var dateTime = DateTimeOffset.FromUnixTimeMilliseconds(value).UtcDateTime;
20 |
21 | writer.WriteString("updatedAt", dateTime);
22 | }
23 | }
24 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Model/Condition.cs:
--------------------------------------------------------------------------------
1 | namespace FeatBit.Sdk.Server.Model
2 | {
3 | internal sealed class Condition
4 | {
5 | public string Property { get; set; }
6 |
7 | public string Op { get; set; }
8 |
9 | public string Value { get; set; }
10 | }
11 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Model/FbUser.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace FeatBit.Sdk.Server.Model
4 | {
5 | public class FbUser
6 | {
7 | public readonly string Key;
8 | public readonly string Name;
9 | public readonly Dictionary Custom;
10 |
11 | internal FbUser(string key, string name, Dictionary custom)
12 | {
13 | Key = key;
14 | Name = name;
15 | Custom = custom;
16 | }
17 |
18 | ///
19 | /// Creates an for constructing a user object using a fluent syntax.
20 | ///
21 | ///
22 | /// This is the only method for building a . The has methods
23 | /// for setting any number of properties, after which you call to get the
24 | /// resulting instance.
25 | ///
26 | ///
27 | ///
28 | /// var user = FbUser.Builder("a-unique-key-of-user")
29 | /// .Name("user-name")
30 | /// .Custom("email", "test@example.com")
31 | /// .Build();
32 | ///
33 | ///
34 | /// a that uniquely identifies a user
35 | /// a builder object
36 | public static IFbUserBuilder Builder(string key)
37 | {
38 | return new FbUserBuilder(key);
39 | }
40 |
41 | public string ValueOf(string property)
42 | {
43 | if (string.IsNullOrWhiteSpace(property))
44 | {
45 | return string.Empty;
46 | }
47 |
48 | if (property == "keyId")
49 | {
50 | return Key;
51 | }
52 |
53 | if (property == "name")
54 | {
55 | return Name;
56 | }
57 |
58 | return Custom.TryGetValue(property, out var value) ? value : string.Empty;
59 | }
60 | }
61 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Model/FbUserBuilder.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace FeatBit.Sdk.Server.Model
5 | {
6 | public interface IFbUserBuilder
7 | {
8 | ///
9 | /// Creates a based on the properties that have been set on the builder.
10 | /// Modifying the builder after this point does not affect the returned .
11 | ///
12 | /// the configured object
13 | FbUser Build();
14 |
15 | ///
16 | /// Sets the full name for a user.
17 | ///
18 | /// the name for the user
19 | /// the same builder
20 | IFbUserBuilder Name(string name);
21 |
22 | ///
23 | /// Adds a custom attribute with a string value.
24 | ///
25 | /// the key for the custom attribute
26 | /// the value for the custom attribute
27 | /// the same builder
28 | IFbUserBuilder Custom(string key, string value);
29 | }
30 |
31 | internal class FbUserBuilder : IFbUserBuilder
32 | {
33 | private readonly string _key;
34 | private string _name;
35 | private readonly Dictionary _custom;
36 |
37 | public FbUserBuilder(string key)
38 | {
39 | _key = key;
40 | _name = string.Empty;
41 | _custom = new Dictionary();
42 | }
43 |
44 | public FbUser Build()
45 | {
46 | return new FbUser(_key, _name, _custom);
47 | }
48 |
49 | public IFbUserBuilder Name(string name)
50 | {
51 | if (!string.IsNullOrWhiteSpace(name))
52 | {
53 | _name = name;
54 | }
55 |
56 | return this;
57 | }
58 |
59 | public IFbUserBuilder Custom(string key, string value)
60 | {
61 | if (string.IsNullOrWhiteSpace(key))
62 | {
63 | throw new ArgumentException("key cannot be null or empty");
64 | }
65 |
66 | _custom[key] = value;
67 | return this;
68 | }
69 | }
70 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Model/FeatureFlag.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using FeatBit.Sdk.Server.Evaluation;
5 | using FeatBit.Sdk.Server.Store;
6 |
7 | namespace FeatBit.Sdk.Server.Model
8 | {
9 | internal sealed class FeatureFlag : StorableObject
10 | {
11 | public override string StoreKey => StoreKeys.ForFeatureFlag(Key);
12 |
13 | public Guid Id { get; set; }
14 |
15 | public string Key { get; set; }
16 |
17 | public string VariationType { get; set; }
18 |
19 | public ICollection Variations { get; set; }
20 |
21 | public ICollection TargetUsers { get; set; }
22 |
23 | public ICollection Rules { get; set; }
24 |
25 | public bool IsEnabled { get; set; }
26 |
27 | public string DisabledVariationId { get; set; }
28 |
29 | public Fallthrough Fallthrough { get; set; }
30 |
31 | public bool ExptIncludeAllTargets { get; set; }
32 |
33 | #if NETCOREAPP3_1
34 | // for deserialization
35 | public FeatureFlag()
36 | {
37 | }
38 | #endif
39 |
40 | public FeatureFlag(
41 | Guid id,
42 | string key,
43 | long version,
44 | string variationType,
45 | ICollection variations,
46 | ICollection targetUsers,
47 | ICollection rules,
48 | bool isEnabled,
49 | string disabledVariationId,
50 | Fallthrough fallthrough,
51 | bool exptIncludeAllTargets)
52 | {
53 | Id = id;
54 | Key = key;
55 | Version = version;
56 | VariationType = variationType;
57 | Variations = variations;
58 | TargetUsers = targetUsers;
59 | Rules = rules;
60 | IsEnabled = isEnabled;
61 | DisabledVariationId = disabledVariationId;
62 | Fallthrough = fallthrough;
63 | ExptIncludeAllTargets = exptIncludeAllTargets;
64 | }
65 |
66 | public Variation GetVariation(string variationId)
67 | {
68 | return Variations.FirstOrDefault(x => x.Id == variationId);
69 | }
70 | }
71 |
72 | internal sealed class TargetUser
73 | {
74 | public ICollection KeyIds { get; set; }
75 |
76 | public string VariationId { get; set; }
77 | }
78 |
79 | internal sealed class TargetRule
80 | {
81 | public string Name { get; set; }
82 |
83 | public string DispatchKey { get; set; }
84 |
85 | public bool IncludedInExpt { get; set; }
86 |
87 | public ICollection Conditions { get; set; }
88 |
89 | public ICollection Variations { get; set; }
90 | }
91 |
92 | internal sealed class Fallthrough
93 | {
94 | public string DispatchKey { get; set; }
95 |
96 | public bool IncludedInExpt { get; set; }
97 |
98 | public ICollection Variations { get; set; }
99 | }
100 |
101 | internal sealed class RolloutVariation
102 | {
103 | public string Id { get; set; }
104 |
105 | public double[] Rollout { get; set; }
106 |
107 | public double ExptRollout { get; set; }
108 |
109 | public bool IsInRollout(string key) => DispatchAlgorithm.IsInRollout(key, Rollout);
110 |
111 | public double DispatchRollout()
112 | {
113 | if (Rollout is not { Length: 2 })
114 | {
115 | // malformed rollout
116 | return 0.0;
117 | }
118 |
119 | return Rollout[1] - Rollout[0];
120 | }
121 | }
122 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Model/FeatureFlagBuilder.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace FeatBit.Sdk.Server.Model
5 | {
6 | internal class FeatureFlagBuilder
7 | {
8 | private Guid _id = Guid.NewGuid();
9 | private string _key;
10 | private long _version;
11 | private string _variationType = string.Empty;
12 | private ICollection _variations = new List();
13 | private ICollection _targetUsers = new List();
14 | private ICollection _rules = new List();
15 | private bool _isEnabled = true;
16 | private string _disabledVariationId = string.Empty;
17 | private Fallthrough _fallthrough;
18 | private bool _exptIncludeAllTargets = true;
19 |
20 | public FeatureFlag Build()
21 | {
22 | return new FeatureFlag(_id, _key, _version, _variationType, _variations, _targetUsers, _rules, _isEnabled,
23 | _disabledVariationId, _fallthrough, _exptIncludeAllTargets);
24 | }
25 |
26 | public FeatureFlagBuilder Id(Guid id)
27 | {
28 | _id = id;
29 | return this;
30 | }
31 |
32 | public FeatureFlagBuilder Key(string key)
33 | {
34 | _key = key;
35 | return this;
36 | }
37 |
38 | public FeatureFlagBuilder Version(long version)
39 | {
40 | _version = version;
41 | return this;
42 | }
43 |
44 | public FeatureFlagBuilder VariationType(string variationType)
45 | {
46 | _variationType = variationType;
47 | return this;
48 | }
49 |
50 | public FeatureFlagBuilder Variations(params Variation[] variations)
51 | {
52 | foreach (var variation in variations)
53 | {
54 | _variations.Add(variation);
55 | }
56 |
57 | return this;
58 | }
59 |
60 | public FeatureFlagBuilder Variations(List variations)
61 | {
62 | _variations = variations;
63 | return this;
64 | }
65 |
66 | public FeatureFlagBuilder TargetUsers(params TargetUser[] targetUsers)
67 | {
68 | foreach (var targetUser in targetUsers)
69 | {
70 | _targetUsers.Add(targetUser);
71 | }
72 |
73 | return this;
74 | }
75 |
76 | public FeatureFlagBuilder TargetUsers(List targetUsers)
77 | {
78 | _targetUsers = targetUsers;
79 | return this;
80 | }
81 |
82 | public FeatureFlagBuilder Rules(params TargetRule[] rules)
83 | {
84 | foreach (var rule in rules)
85 | {
86 | _rules.Add(rule);
87 | }
88 |
89 | return this;
90 | }
91 |
92 | public FeatureFlagBuilder Rules(List rules)
93 | {
94 | _rules = rules;
95 | return this;
96 | }
97 |
98 | public FeatureFlagBuilder IsEnabled(bool isEnabled)
99 | {
100 | _isEnabled = isEnabled;
101 | return this;
102 | }
103 |
104 | public FeatureFlagBuilder DisabledVariationId(string disabledVariationId)
105 | {
106 | _disabledVariationId = disabledVariationId;
107 | return this;
108 | }
109 |
110 | public FeatureFlagBuilder Fallthrough(Fallthrough fallthrough)
111 | {
112 | _fallthrough = fallthrough;
113 | return this;
114 | }
115 |
116 | public FeatureFlagBuilder ExptIncludeAllTargets(bool exptIncludeAllTargets)
117 | {
118 | _exptIncludeAllTargets = exptIncludeAllTargets;
119 | return this;
120 | }
121 | }
122 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Model/Segment.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using FeatBit.Sdk.Server.Store;
4 |
5 | namespace FeatBit.Sdk.Server.Model
6 | {
7 | internal sealed class Segment : StorableObject
8 | {
9 | public override string StoreKey => StoreKeys.ForSegment(Id.ToString());
10 |
11 | public Guid Id { get; set; }
12 |
13 | public ICollection Included { get; set; }
14 |
15 | public ICollection Excluded { get; set; }
16 |
17 | public ICollection Rules { get; set; }
18 |
19 | #if NETCOREAPP3_1
20 | public Segment()
21 | {
22 | }
23 | #endif
24 |
25 | public Segment(
26 | Guid id,
27 | long version,
28 | ICollection included,
29 | ICollection excluded,
30 | ICollection rules)
31 | {
32 | Id = id;
33 | Version = version;
34 | Included = included;
35 | Excluded = excluded;
36 | Rules = rules;
37 | }
38 | }
39 |
40 | internal sealed class MatchRule
41 | {
42 | public ICollection Conditions { get; set; } = Array.Empty();
43 | }
44 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Model/SegmentBuilder.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace FeatBit.Sdk.Server.Model
5 | {
6 | internal class SegmentBuilder
7 | {
8 | private Guid _id;
9 | private long _version;
10 | private ICollection _included = new List();
11 | private ICollection _excluded = new List();
12 | private ICollection _rules = new List();
13 |
14 | public Segment Build()
15 | {
16 | return new Segment(_id, _version, _included, _excluded, _rules);
17 | }
18 |
19 | public SegmentBuilder Id(Guid id)
20 | {
21 | _id = id;
22 | return this;
23 | }
24 |
25 | public SegmentBuilder Version(long version)
26 | {
27 | _version = version;
28 | return this;
29 | }
30 |
31 | public SegmentBuilder Included(params string[] keys)
32 | {
33 | foreach (var key in keys)
34 | {
35 | _included.Add(key);
36 | }
37 |
38 | return this;
39 | }
40 |
41 | public SegmentBuilder Excluded(params string[] keys)
42 | {
43 | foreach (var key in keys)
44 | {
45 | _excluded.Add(key);
46 | }
47 |
48 | return this;
49 | }
50 |
51 | public SegmentBuilder Rules(List rules)
52 | {
53 | _rules = rules;
54 | return this;
55 | }
56 |
57 | public SegmentBuilder Rules(params MatchRule[] rules)
58 | {
59 | foreach (var rule in rules)
60 | {
61 | _rules.Add(rule);
62 | }
63 |
64 | return this;
65 | }
66 | }
67 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Model/Variation.cs:
--------------------------------------------------------------------------------
1 | namespace FeatBit.Sdk.Server.Model
2 | {
3 | internal sealed class Variation
4 | {
5 | public string Id { get; set; }
6 |
7 | public string Value { get; set; }
8 |
9 | public static readonly Variation Empty = new()
10 | {
11 | Id = string.Empty,
12 | Value = string.Empty
13 | };
14 | }
15 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/NuGet.md:
--------------------------------------------------------------------------------
1 | ## About
2 |
3 | This is the .NET Server-Side SDK for the 100% open-source feature flags management
4 | platform [FeatBit](https://github.com/featbit/featbit).
5 |
6 | More documentation is available at the [SDK website](https://github.com/featbit/dotnet-server-sdk).
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.CompilerServices;
2 |
3 | [assembly: InternalsVisibleTo("FeatBit.ServerSdk.Tests")]
4 | [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Retry/BackoffAndJitterRetryPolicy.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace FeatBit.Sdk.Server.Retry
4 | {
5 | public class BackoffAndJitterRetryPolicy : IRetryPolicy
6 | {
7 | private static readonly Random Random = new Random();
8 |
9 | private readonly TimeSpan _firstRetryDelay;
10 | private readonly TimeSpan _maxRetryDelay;
11 | private readonly double _jitterRatio;
12 |
13 | private static readonly TimeSpan DefaultFirstRetryDelay = TimeSpan.FromSeconds(1);
14 | private static readonly TimeSpan DefaultMaxRetryDelay = TimeSpan.FromSeconds(30);
15 |
16 | public BackoffAndJitterRetryPolicy(TimeSpan? firstRetryDelay = null, TimeSpan? maxRetryDelay = null)
17 | {
18 | _firstRetryDelay = firstRetryDelay ?? DefaultFirstRetryDelay;
19 | _maxRetryDelay = maxRetryDelay ?? DefaultMaxRetryDelay;
20 | _jitterRatio = 0.5d;
21 | }
22 |
23 | public TimeSpan NextRetryDelay(RetryContext retryContext)
24 | {
25 | var backoffTime = Backoff(retryContext.RetryAttempt);
26 | var delay = (long)Math.Round(Jitter(backoffTime) + backoffTime / 2);
27 |
28 | return TimeSpan.FromMilliseconds(delay);
29 | }
30 |
31 | private double Backoff(int retryAttempt)
32 | {
33 | var delay = _firstRetryDelay.TotalMilliseconds * Math.Pow(2, retryAttempt);
34 | var max = _maxRetryDelay.TotalMilliseconds;
35 | return Math.Min(delay, max);
36 | }
37 |
38 | private double Jitter(double backoff)
39 | {
40 | return backoff * _jitterRatio * Random.NextDouble();
41 | }
42 | }
43 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Retry/DefaultRetryPolicy.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace FeatBit.Sdk.Server.Retry
5 | {
6 | internal sealed class DefaultRetryPolicy : IRetryPolicy
7 | {
8 | internal static readonly TimeSpan[] DefaultRetryDelays =
9 | {
10 | // retry immediately for the first
11 | TimeSpan.Zero,
12 | TimeSpan.FromSeconds(1),
13 | TimeSpan.FromSeconds(2),
14 | TimeSpan.FromSeconds(3),
15 | TimeSpan.FromSeconds(5),
16 | TimeSpan.FromSeconds(8),
17 | TimeSpan.FromSeconds(13),
18 | TimeSpan.FromSeconds(21),
19 | TimeSpan.FromSeconds(34),
20 | TimeSpan.FromSeconds(55)
21 | };
22 |
23 | private readonly TimeSpan[] _retryDelays;
24 |
25 | public DefaultRetryPolicy()
26 | {
27 | _retryDelays = DefaultRetryDelays;
28 | }
29 |
30 | public DefaultRetryPolicy(IReadOnlyList retryDelays)
31 | {
32 | if (retryDelays == null || retryDelays.Count == 0)
33 | {
34 | throw new ArgumentException("retry delays cannot be null or empty", nameof(retryDelays));
35 | }
36 |
37 | _retryDelays = new TimeSpan[retryDelays.Count];
38 | for (var i = 0; i < retryDelays.Count; i++)
39 | {
40 | _retryDelays[i] = retryDelays[i];
41 | }
42 | }
43 |
44 | public TimeSpan NextRetryDelay(RetryContext retryContext)
45 | {
46 | var index = retryContext.RetryAttempt % _retryDelays.Length;
47 | return _retryDelays[index];
48 | }
49 | }
50 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Retry/IRetryPolicy.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace FeatBit.Sdk.Server.Retry
4 | {
5 | ///
6 | /// An abstraction that controls when the client attempts to reconnect and how many times it does so.
7 | ///
8 | public interface IRetryPolicy
9 | {
10 | ///
11 | /// this will be called after the transport loses a connection to determine if and for how long to wait before the next reconnect attempt.
12 | ///
13 | ///
14 | /// Information related to the next possible reconnect attempt including the number of consecutive failed retries so far
15 | ///
16 | ///
17 | /// A representing the amount of time to wait from now before starting the next reconnect attempt.
18 | ///
19 | TimeSpan NextRetryDelay(RetryContext retryContext);
20 | }
21 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Retry/RetryContext.cs:
--------------------------------------------------------------------------------
1 | namespace FeatBit.Sdk.Server.Retry
2 | {
3 | ///
4 | /// The context passed to to help the policy determine
5 | /// how long to wait before the next retry and whether there should be another retry at all.
6 | ///
7 | public class RetryContext
8 | {
9 | ///
10 | /// The number of consecutive failed retries so far.
11 | ///
12 | public int RetryAttempt { get; set; }
13 | }
14 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Store/DefaultMemoryStore.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 |
5 | namespace FeatBit.Sdk.Server.Store
6 | {
7 | public class DefaultMemoryStore : IMemoryStore
8 | {
9 | public bool Populated { get; private set; }
10 |
11 | private readonly object _writeLock = new object();
12 |
13 | private volatile Dictionary _items =
14 | new Dictionary();
15 |
16 | public void Populate(IEnumerable objects)
17 | {
18 | lock (_writeLock)
19 | {
20 | _items = objects.ToDictionary(storableObj => storableObj.StoreKey, storableObj => storableObj);
21 | Populated = true;
22 | }
23 | }
24 |
25 | public TObject Get(string key) where TObject : class
26 | {
27 | if (_items.TryGetValue(key, out var obj) && obj is TObject tObject)
28 | {
29 | return tObject;
30 | }
31 |
32 | return null;
33 | }
34 |
35 | public ICollection Find(Func predicate)
36 | {
37 | var result = new List();
38 |
39 | foreach (var value in _items.Values.Where(predicate))
40 | {
41 | if (value is TObject tObject)
42 | {
43 | result.Add(tObject);
44 | }
45 | }
46 |
47 | return result;
48 | }
49 |
50 | public bool Upsert(StorableObject storableObj)
51 | {
52 | var key = storableObj.StoreKey;
53 |
54 | lock (_writeLock)
55 | {
56 | // update item
57 | if (_items.TryGetValue(key, out var existed))
58 | {
59 | if (existed.Version >= storableObj.Version)
60 | {
61 | return false;
62 | }
63 |
64 | _items[key] = storableObj;
65 | return true;
66 | }
67 |
68 | // add item
69 | _items.Add(key, storableObj);
70 | return true;
71 | }
72 | }
73 |
74 | public long Version()
75 | {
76 | var values = _items.Values;
77 |
78 | return values.Count == 0 ? 0 : values.Max(x => x.Version);
79 | }
80 | }
81 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Store/IMemoryStore.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace FeatBit.Sdk.Server.Store
5 | {
6 | ///
7 | /// Interface for a data storage that holds feature flags, segments or any other related data received by the SDK.
8 | ///
9 | ///
10 | ///
11 | /// Implementations must be thread-safe.
12 | ///
13 | ///
14 | public interface IMemoryStore
15 | {
16 | ///
17 | /// Indicates whether this store has been populated with any data yet.
18 | ///
19 | /// true if the store contains data
20 | bool Populated { get; }
21 |
22 | ///
23 | /// Overwrites the store's contents with a set of new items.
24 | ///
25 | ///
26 | ///
27 | /// All previous data will be discarded, regardless of versioning.
28 | ///
29 | ///
30 | /// a list of instances with
31 | /// their store keys.
32 | void Populate(IEnumerable objects);
33 |
34 | ///
35 | /// Retrieves an object from the store, if available.
36 | ///
37 | /// the unique key of the object within the store
38 | /// The object; null if the key is unknown
39 | TObject Get(string key) where TObject : class;
40 |
41 | ///
42 | /// Retrieves all the objects that match the conditions defined by the specified predicate.
43 | ///
44 | /// The delegate which defines the conditions of the elements to search for.
45 | /// A containing all the elements that match the conditions defined by the specified predicate, if found;
46 | /// otherwise, an empty
47 | ICollection Find(Func predicate);
48 |
49 | ///
50 | /// Updates or inserts an item in the store. For updates, the object will only be
51 | /// updated if the existing version is less than the new version.
52 | ///
53 | /// the item to insert or update
54 | /// true if the item was updated; false if it was not updated because the
55 | /// store contains an equal or greater version
56 | bool Upsert(StorableObject storableObj);
57 |
58 | ///
59 | /// Get the version of a storage
60 | ///
61 | /// a long value represents the version of a storage
62 | long Version();
63 | }
64 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Store/StorableObject.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json.Serialization;
2 | using FeatBit.Sdk.Server.Json;
3 |
4 | namespace FeatBit.Sdk.Server.Store
5 | {
6 | public abstract class StorableObject
7 | {
8 | [JsonPropertyName("updatedAt")]
9 | [JsonConverter(typeof(VersionJsonConverter))]
10 | public long Version { get; protected set; }
11 |
12 | public abstract string StoreKey { get; }
13 | }
14 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Store/StoreKeys.cs:
--------------------------------------------------------------------------------
1 | namespace FeatBit.Sdk.Server.Store
2 | {
3 | internal static class StoreKeys
4 | {
5 | public const string SegmentPrefix = "segment_";
6 | public const string FlagPrefix = "ff_";
7 |
8 | public static string ForSegment(string segmentId)
9 | {
10 | return $"{SegmentPrefix}{segmentId}";
11 | }
12 |
13 | public static string ForFeatureFlag(string flagKey)
14 | {
15 | return $"{FlagPrefix}{flagKey}";
16 | }
17 | }
18 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Transport/ConnectionToken.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace FeatBit.Sdk.Server.Transport
6 | {
7 | internal static class ConnectionToken
8 | {
9 | private static readonly Dictionary NumberMap = new Dictionary()
10 | {
11 | { '0', 'Q' },
12 | { '1', 'B' },
13 | { '2', 'W' },
14 | { '3', 'S' },
15 | { '4', 'P' },
16 | { '5', 'H' },
17 | { '6', 'D' },
18 | { '7', 'X' },
19 | { '8', 'Z' },
20 | { '9', 'U' }
21 | };
22 |
23 | internal static string New(string envSecret)
24 | {
25 | var trimmed = envSecret.TrimEnd('=');
26 | var timestamp = new DateTimeOffset(DateTime.UtcNow).ToUnixTimeMilliseconds();
27 | var timestampLength = timestamp.ToString().Length;
28 | var start = Math.Max((int)Math.Floor(new Random().NextDouble() * trimmed.Length), 2);
29 |
30 | var sb = new StringBuilder();
31 |
32 | sb.Append(EncodeNumber(start, 3));
33 | sb.Append(EncodeNumber(timestamp.ToString().Length, 2));
34 | sb.Append(trimmed.Substring(0, start));
35 | sb.Append(EncodeNumber(timestamp, timestampLength));
36 | sb.Append(trimmed.Substring(start));
37 |
38 | return sb.ToString();
39 | }
40 |
41 | private static string EncodeNumber(long number, int length)
42 | {
43 | var sb = new StringBuilder();
44 |
45 | var paddedNumber = number.ToString().PadLeft(length, '0');
46 | foreach (var n in paddedNumber.Substring(paddedNumber.Length - length))
47 | {
48 | sb.Append(NumberMap[n]);
49 | }
50 |
51 | return sb.ToString();
52 | }
53 | }
54 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Transport/DuplexPipe.cs:
--------------------------------------------------------------------------------
1 | using System.IO.Pipelines;
2 |
3 | namespace FeatBit.Sdk.Server.Transport
4 | {
5 | internal sealed class DuplexPipe : IDuplexPipe
6 | {
7 | public DuplexPipe(PipeReader reader, PipeWriter writer)
8 | {
9 | Input = reader;
10 | Output = writer;
11 | }
12 |
13 | public PipeReader Input { get; }
14 |
15 | public PipeWriter Output { get; }
16 |
17 | public static DuplexPipePair CreateConnectionPair(PipeOptions transportPipeOptions, PipeOptions appPipeOptions)
18 | {
19 | var transport = new Pipe(transportPipeOptions);
20 | var application = new Pipe(appPipeOptions);
21 |
22 | var transportToApplication = new DuplexPipe(application.Reader, transport.Writer);
23 | var applicationToTransport = new DuplexPipe(transport.Reader, application.Writer);
24 |
25 | return new DuplexPipePair(applicationToTransport, transportToApplication);
26 | }
27 |
28 | // This class exists to work around issues with value tuple on .NET Framework
29 | public readonly struct DuplexPipePair
30 | {
31 | public IDuplexPipe Transport { get; }
32 | public IDuplexPipe Application { get; }
33 |
34 | public DuplexPipePair(IDuplexPipe transport, IDuplexPipe application)
35 | {
36 | Transport = transport;
37 | Application = application;
38 | }
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Transport/TextMessageFormatter.cs:
--------------------------------------------------------------------------------
1 | using System.Buffers;
2 |
3 | namespace FeatBit.Sdk.Server.Transport
4 | {
5 | internal static class TextMessageFormatter
6 | {
7 | // This record separator is supposed to be used only for JSON payloads where 0x1e character
8 | // will not occur (is not a valid character) and therefore it is safe to not escape it
9 | public const byte RecordSeparator = 0x1e;
10 |
11 | public static void WriteRecordSeparator(IBufferWriter output)
12 | {
13 | var buffer = output.GetSpan(1);
14 | buffer[0] = RecordSeparator;
15 | output.Advance(1);
16 | }
17 | }
18 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Transport/TextMessageParser.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Buffers;
3 | using System.Runtime.CompilerServices;
4 |
5 | namespace FeatBit.Sdk.Server.Transport
6 | {
7 | internal static class TextMessageParser
8 | {
9 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
10 | public static bool TryParseMessage(ref ReadOnlySequence buffer, out ReadOnlySequence payload)
11 | {
12 | if (buffer.IsSingleSegment)
13 | {
14 | var span = buffer.First.Span;
15 | var index = span.IndexOf(TextMessageFormatter.RecordSeparator);
16 | if (index == -1)
17 | {
18 | payload = default;
19 | return false;
20 | }
21 |
22 | payload = buffer.Slice(0, index);
23 |
24 | buffer = buffer.Slice(index + 1);
25 |
26 | return true;
27 | }
28 | else
29 | {
30 | return TryParseMessageMultiSegment(ref buffer, out payload);
31 | }
32 | }
33 |
34 | private static bool TryParseMessageMultiSegment(ref ReadOnlySequence buffer, out ReadOnlySequence payload)
35 | {
36 | var position = buffer.PositionOf(TextMessageFormatter.RecordSeparator);
37 | if (position == null)
38 | {
39 | payload = default;
40 | return false;
41 | }
42 |
43 | payload = buffer.Slice(0, position.Value);
44 |
45 | // Skip record separator
46 | buffer = buffer.Slice(buffer.GetPosition(1, position.Value));
47 |
48 | return true;
49 | }
50 | }
51 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Transport/WebSocketExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.Buffers;
2 | using System.Diagnostics;
3 | using System.Net.WebSockets;
4 | using System.Runtime.InteropServices;
5 | using System.Threading;
6 | using System.Threading.Tasks;
7 |
8 | namespace FeatBit.Sdk.Server.Transport
9 | {
10 | internal static class WebSocketExtensions
11 | {
12 | public static ValueTask SendAsync(this WebSocket webSocket, ReadOnlySequence buffer,
13 | WebSocketMessageType webSocketMessageType, CancellationToken cancellationToken = default)
14 | {
15 | #if NETCOREAPP
16 | if (buffer.IsSingleSegment)
17 | {
18 | return webSocket.SendAsync(buffer.First, webSocketMessageType, endOfMessage: true, cancellationToken);
19 | }
20 | else
21 | {
22 | return SendMultiSegmentAsync(webSocket, buffer, webSocketMessageType, cancellationToken);
23 | }
24 | #else
25 | if (buffer.IsSingleSegment)
26 | {
27 | var isArray = MemoryMarshal.TryGetArray(buffer.First, out var segment);
28 | Debug.Assert(isArray);
29 | return new ValueTask(webSocket.SendAsync(segment, webSocketMessageType, endOfMessage: true,
30 | cancellationToken));
31 | }
32 | else
33 | {
34 | return SendMultiSegmentAsync(webSocket, buffer, webSocketMessageType, cancellationToken);
35 | }
36 | #endif
37 | }
38 |
39 | private static async ValueTask SendMultiSegmentAsync(WebSocket webSocket, ReadOnlySequence buffer,
40 | WebSocketMessageType webSocketMessageType, CancellationToken cancellationToken = default)
41 | {
42 | var position = buffer.Start;
43 | // Get a segment before the loop so we can be one segment behind while writing
44 | // This allows us to do a non-zero byte write for the endOfMessage = true send
45 | buffer.TryGet(ref position, out var prevSegment);
46 | while (buffer.TryGet(ref position, out var segment))
47 | {
48 | #if NETCOREAPP
49 | await webSocket.SendAsync(prevSegment, webSocketMessageType, endOfMessage: false, cancellationToken)
50 | .ConfigureAwait(false);
51 | #else
52 | var isArray = MemoryMarshal.TryGetArray(prevSegment, out var arraySegment);
53 | Debug.Assert(isArray);
54 | await webSocket.SendAsync(arraySegment, webSocketMessageType, endOfMessage: false, cancellationToken)
55 | .ConfigureAwait(false);
56 | #endif
57 | prevSegment = segment;
58 | }
59 |
60 | // End of message frame
61 | #if NETCOREAPP
62 | await webSocket.SendAsync(prevSegment, webSocketMessageType, endOfMessage: true, cancellationToken)
63 | .ConfigureAwait(false);
64 | #else
65 | var isArrayEnd = MemoryMarshal.TryGetArray(prevSegment, out var arraySegmentEnd);
66 | Debug.Assert(isArrayEnd);
67 | await webSocket.SendAsync(arraySegmentEnd, webSocketMessageType, endOfMessage: true, cancellationToken)
68 | .ConfigureAwait(false);
69 | #endif
70 | }
71 | }
72 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/Transport/WebSocketTransport.Log.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net.WebSockets;
3 | using Microsoft.Extensions.Logging;
4 |
5 | namespace FeatBit.Sdk.Server.Transport;
6 |
7 | internal sealed partial class WebSocketTransport
8 | {
9 | private static partial class Log
10 | {
11 | [LoggerMessage(1, LogLevel.Debug, "Started transport.", EventName = "StartedTransport")]
12 | public static partial void StartedTransport(ILogger logger);
13 |
14 | [LoggerMessage(2, LogLevel.Debug, "WebSocket closed by the server. Close status {CloseStatus}.",
15 | EventName = "WebSocketClosed")]
16 | public static partial void WebSocketClosed(ILogger logger, WebSocketCloseStatus? closeStatus);
17 |
18 | [LoggerMessage(3, LogLevel.Debug, "Message received. Type: {MessageType}, size: {Count}.",
19 | EventName = "MessageReceived")]
20 | public static partial void MessageReceived(ILogger logger, WebSocketMessageType messageType, int count);
21 |
22 | [LoggerMessage(4, LogLevel.Debug, "Receive loop canceled.", EventName = "ReceiveCanceled")]
23 | public static partial void ReceiveCanceled(ILogger logger);
24 |
25 | [LoggerMessage(5, LogLevel.Debug, "Receive loop stopped.", EventName = "ReceiveStopped")]
26 | public static partial void ReceiveStopped(ILogger logger);
27 |
28 | [LoggerMessage(6, LogLevel.Debug, "Received message from application. Payload size: {Count}.",
29 | EventName = "ReceivedFromApp")]
30 | public static partial void ReceivedFromApp(ILogger logger, long count);
31 |
32 | [LoggerMessage(7, LogLevel.Debug, "Error while sending a message.", EventName = "ErrorSendingMessage")]
33 | public static partial void ErrorSendingMessage(ILogger logger, Exception exception);
34 |
35 | [LoggerMessage(8, LogLevel.Debug, "Closing webSocket failed.", EventName = "ClosingWebSocketFailed")]
36 | public static partial void ClosingWebSocketFailed(ILogger logger, Exception exception);
37 |
38 | [LoggerMessage(9, LogLevel.Debug, "Send loop stopped.", EventName = "SendStopped")]
39 | public static partial void SendStopped(ILogger logger);
40 |
41 | [LoggerMessage(10, LogLevel.Debug, "Transport stopped.", EventName = "TransportStopped")]
42 | public static partial void TransportStopped(ILogger logger, Exception exception);
43 | }
44 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/ValueConverters.cs:
--------------------------------------------------------------------------------
1 | namespace FeatBit.Sdk.Server
2 | {
3 | internal delegate bool ValueConverter(string value, out TValue converted);
4 |
5 | internal static class ValueConverters
6 | {
7 | internal static readonly ValueConverter Bool = (string value, out bool converted) =>
8 | bool.TryParse(value, out converted);
9 |
10 | internal static readonly ValueConverter String = (string value, out string converted) =>
11 | {
12 | converted = value;
13 | return true;
14 | };
15 |
16 | public static readonly ValueConverter Int = (string value, out int converted) =>
17 | int.TryParse(value, out converted);
18 |
19 | public static readonly ValueConverter Float = (string value, out float converted) =>
20 | float.TryParse(value, out converted);
21 |
22 | public static readonly ValueConverter Double = (string value, out double converted) =>
23 | double.TryParse(value, out converted);
24 | }
25 | }
--------------------------------------------------------------------------------
/src/FeatBit.ServerSdk/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/featbit/featbit-dotnet-sdk/0cfb2635f135f7b5c77a21865055d042b6f96f66/src/FeatBit.ServerSdk/icon.png
--------------------------------------------------------------------------------
/tests/FeatBit.ServerSdk.Tests/Bootstrapping/JsonBootstrapProviderTests.PopulateStore.verified.txt:
--------------------------------------------------------------------------------
1 | {
2 | flag: {
3 | StoreKey: ff_example-flag,
4 | Id: Guid_1,
5 | Key: example-flag,
6 | VariationType: boolean,
7 | Variations: [
8 | {
9 | Id: Guid_2,
10 | Value: true
11 | },
12 | {
13 | Id: Guid_3,
14 | Value: false
15 | }
16 | ],
17 | TargetUsers: [
18 | {
19 | KeyIds: [
20 | true-1
21 | ],
22 | VariationId: Guid_2
23 | },
24 | {
25 | KeyIds: [
26 | false-1
27 | ],
28 | VariationId: Guid_3
29 | }
30 | ],
31 | Rules: [
32 | {
33 | Name: Rule 1,
34 | IncludedInExpt: false,
35 | Conditions: [
36 | {
37 | Property: age,
38 | Op: BiggerThan,
39 | Value: 22
40 | },
41 | {
42 | Property: name,
43 | Op: EndsWith,
44 | Value: _qa
45 | }
46 | ],
47 | Variations: [
48 | {
49 | Id: Guid_2,
50 | Rollout: [
51 | 0.0,
52 | 1.0
53 | ],
54 | ExptRollout: 1.0
55 | }
56 | ]
57 | },
58 | {
59 | Name: Rule 2,
60 | DispatchKey: keyId,
61 | IncludedInExpt: false,
62 | Conditions: [
63 | {
64 | Property: country,
65 | Op: IsOneOf,
66 | Value: ["cn","us","jp","gb","es","ss"]
67 | }
68 | ],
69 | Variations: [
70 | {
71 | Id: Guid_2,
72 | Rollout: [
73 | 0.0,
74 | 0.2
75 | ],
76 | ExptRollout: 1.0
77 | },
78 | {
79 | Id: Guid_3,
80 | Rollout: [
81 | 0.2,
82 | 1.0
83 | ],
84 | ExptRollout: 1.0
85 | }
86 | ]
87 | }
88 | ],
89 | IsEnabled: true,
90 | DisabledVariationId: Guid_3,
91 | Fallthrough: {
92 | IncludedInExpt: true,
93 | Variations: [
94 | {
95 | Id: Guid_2,
96 | Rollout: [
97 | 0.0,
98 | 1.0
99 | ],
100 | ExptRollout: 1.0
101 | }
102 | ]
103 | },
104 | ExptIncludeAllTargets: true,
105 | Version: 1674871495616
106 | },
107 | segment: {
108 | StoreKey: segment_0779d76b-afc6-4886-ab65-af8c004273ad,
109 | Id: Guid_4,
110 | Included: [
111 | true-1
112 | ],
113 | Excluded: [
114 | false-1
115 | ],
116 | Rules: [
117 | {
118 | Conditions: [
119 | {
120 | Property: age,
121 | Op: LessEqualThan,
122 | Value: 22
123 | },
124 | {
125 | Property: country,
126 | Op: IsOneOf,
127 | Value: ["cn","us","es"]
128 | },
129 | {
130 | Property: name,
131 | Op: NotEqual,
132 | Value: bob
133 | },
134 | {
135 | Property: isMember,
136 | Op: IsTrue,
137 | Value: IsTrue
138 | }
139 | ]
140 | }
141 | ],
142 | Version: 1674885283583
143 | }
144 | }
--------------------------------------------------------------------------------
/tests/FeatBit.ServerSdk.Tests/Bootstrapping/JsonBootstrapProviderTests.UseValidJson.verified.txt:
--------------------------------------------------------------------------------
1 | {
2 | EventType: full,
3 | FeatureFlags: [
4 | {
5 | StoreKey: ff_example-flag,
6 | Id: Guid_1,
7 | Key: example-flag,
8 | VariationType: boolean,
9 | Variations: [
10 | {
11 | Id: Guid_2,
12 | Value: true
13 | },
14 | {
15 | Id: Guid_3,
16 | Value: false
17 | }
18 | ],
19 | TargetUsers: [
20 | {
21 | KeyIds: [
22 | true-1
23 | ],
24 | VariationId: Guid_2
25 | },
26 | {
27 | KeyIds: [
28 | false-1
29 | ],
30 | VariationId: Guid_3
31 | }
32 | ],
33 | Rules: [
34 | {
35 | Name: Rule 1,
36 | IncludedInExpt: false,
37 | Conditions: [
38 | {
39 | Property: age,
40 | Op: BiggerThan,
41 | Value: 22
42 | },
43 | {
44 | Property: name,
45 | Op: EndsWith,
46 | Value: _qa
47 | }
48 | ],
49 | Variations: [
50 | {
51 | Id: Guid_2,
52 | Rollout: [
53 | 0.0,
54 | 1.0
55 | ],
56 | ExptRollout: 1.0
57 | }
58 | ]
59 | },
60 | {
61 | Name: Rule 2,
62 | DispatchKey: keyId,
63 | IncludedInExpt: false,
64 | Conditions: [
65 | {
66 | Property: country,
67 | Op: IsOneOf,
68 | Value: ["cn","us","jp","gb","es","ss"]
69 | }
70 | ],
71 | Variations: [
72 | {
73 | Id: Guid_2,
74 | Rollout: [
75 | 0.0,
76 | 0.2
77 | ],
78 | ExptRollout: 1.0
79 | },
80 | {
81 | Id: Guid_3,
82 | Rollout: [
83 | 0.2,
84 | 1.0
85 | ],
86 | ExptRollout: 1.0
87 | }
88 | ]
89 | }
90 | ],
91 | IsEnabled: true,
92 | DisabledVariationId: Guid_3,
93 | Fallthrough: {
94 | IncludedInExpt: true,
95 | Variations: [
96 | {
97 | Id: Guid_2,
98 | Rollout: [
99 | 0.0,
100 | 1.0
101 | ],
102 | ExptRollout: 1.0
103 | }
104 | ]
105 | },
106 | ExptIncludeAllTargets: true,
107 | Version: 1674871495616
108 | }
109 | ],
110 | Segments: [
111 | {
112 | StoreKey: segment_0779d76b-afc6-4886-ab65-af8c004273ad,
113 | Id: Guid_4,
114 | Included: [
115 | true-1
116 | ],
117 | Excluded: [
118 | false-1
119 | ],
120 | Rules: [
121 | {
122 | Conditions: [
123 | {
124 | Property: age,
125 | Op: LessEqualThan,
126 | Value: 22
127 | },
128 | {
129 | Property: country,
130 | Op: IsOneOf,
131 | Value: ["cn","us","es"]
132 | },
133 | {
134 | Property: name,
135 | Op: NotEqual,
136 | Value: bob
137 | },
138 | {
139 | Property: isMember,
140 | Op: IsTrue,
141 | Value: IsTrue
142 | }
143 | ]
144 | }
145 | ],
146 | Version: 1674885283583
147 | }
148 | ]
149 | }
--------------------------------------------------------------------------------
/tests/FeatBit.ServerSdk.Tests/Bootstrapping/JsonBootstrapProviderTests.cs:
--------------------------------------------------------------------------------
1 | using FeatBit.Sdk.Server.Model;
2 | using FeatBit.Sdk.Server.Store;
3 |
4 | namespace FeatBit.Sdk.Server.Bootstrapping;
5 |
6 | [UsesVerify]
7 | public class JsonBootstrapProviderTests
8 | {
9 | [Fact]
10 | public async Task UseValidJson()
11 | {
12 | var provider = new JsonBootstrapProvider(TestData.BootstrapJson);
13 |
14 | var dataSet = provider.DataSet();
15 | await Verify(dataSet);
16 | }
17 |
18 | [Fact]
19 | public void UseInvalidJson()
20 | {
21 | Assert.ThrowsAny(() => new JsonBootstrapProvider("{"));
22 | }
23 |
24 | [Fact]
25 | public async Task PopulateStore()
26 | {
27 | var provider = new JsonBootstrapProvider(TestData.BootstrapJson);
28 | var store = new DefaultMemoryStore();
29 |
30 | provider.Populate(store);
31 |
32 | Assert.True(store.Populated);
33 |
34 | var flag = store.Get("ff_example-flag");
35 | var segment = store.Get("segment_0779d76b-afc6-4886-ab65-af8c004273ad");
36 |
37 | await Verify(new { flag, segment });
38 | }
39 | }
--------------------------------------------------------------------------------
/tests/FeatBit.ServerSdk.Tests/Bootstrapping/featbit-bootstrap.json:
--------------------------------------------------------------------------------
1 | {
2 | "messageType": "data-sync",
3 | "data": {
4 | "eventType": "full",
5 | "featureFlags": [
6 | {
7 | "id": "174c7138-426d-4434-8a91-af8c00306de3",
8 | "createdAt": "2023-01-16T02:56:19.531Z",
9 | "updatedAt": "2023-01-28T02:04:55.616Z",
10 | "creatorId": "197dea2a-1044-497d-8f1e-f01d2e15a756",
11 | "updatorId": "197dea2a-1044-497d-8f1e-f01d2e15a756",
12 | "envId": "226b9bf8-4af3-4ffa-9b01-162270e4cd40",
13 | "name": "example flag",
14 | "key": "example-flag",
15 | "variationType": "boolean",
16 | "variations": [
17 | {
18 | "id": "3da96792-debf-4878-905a-c9b5f9178cd0",
19 | "value": "true"
20 | },
21 | {
22 | "id": "19cf9fb6-b790-4877-ab96-b5c5e73e0b46",
23 | "value": "false"
24 | }
25 | ],
26 | "targetUsers": [
27 | {
28 | "keyIds": [
29 | "true-1"
30 | ],
31 | "variationId": "3da96792-debf-4878-905a-c9b5f9178cd0"
32 | },
33 | {
34 | "keyIds": [
35 | "false-1"
36 | ],
37 | "variationId": "19cf9fb6-b790-4877-ab96-b5c5e73e0b46"
38 | }
39 | ],
40 | "rules": [
41 | {
42 | "id": "16273e69-8e1f-4828-bb9b-07576724f6d8",
43 | "name": "Rule 1",
44 | "dispatchKey": null,
45 | "includedInExpt": false,
46 | "conditions": [
47 | {
48 | "property": "age",
49 | "op": "BiggerThan",
50 | "value": "22"
51 | },
52 | {
53 | "property": "name",
54 | "op": "EndsWith",
55 | "value": "_qa"
56 | }
57 | ],
58 | "variations": [
59 | {
60 | "id": "3da96792-debf-4878-905a-c9b5f9178cd0",
61 | "rollout": [
62 | 0,
63 | 1
64 | ],
65 | "exptRollout": 1
66 | }
67 | ]
68 | },
69 | {
70 | "id": "05a6e5fa-6fb5-4bf8-8c2d-d556ed44c143",
71 | "name": "Rule 2",
72 | "dispatchKey": "keyId",
73 | "includedInExpt": false,
74 | "conditions": [
75 | {
76 | "property": "country",
77 | "op": "IsOneOf",
78 | "value": "[\"cn\",\"us\",\"jp\",\"gb\",\"es\",\"ss\"]"
79 | }
80 | ],
81 | "variations": [
82 | {
83 | "id": "3da96792-debf-4878-905a-c9b5f9178cd0",
84 | "rollout": [
85 | 0,
86 | 0.2
87 | ],
88 | "exptRollout": 1
89 | },
90 | {
91 | "id": "19cf9fb6-b790-4877-ab96-b5c5e73e0b46",
92 | "rollout": [
93 | 0.2,
94 | 1
95 | ],
96 | "exptRollout": 1
97 | }
98 | ]
99 | }
100 | ],
101 | "isEnabled": true,
102 | "disabledVariationId": "19cf9fb6-b790-4877-ab96-b5c5e73e0b46",
103 | "fallthrough": {
104 | "dispatchKey": null,
105 | "includedInExpt": true,
106 | "variations": [
107 | {
108 | "id": "3da96792-debf-4878-905a-c9b5f9178cd0",
109 | "rollout": [
110 | 0,
111 | 1
112 | ],
113 | "exptRollout": 1
114 | }
115 | ]
116 | },
117 | "exptIncludeAllTargets": true,
118 | "tags": [],
119 | "isArchived": false
120 | }
121 | ],
122 | "segments": [
123 | {
124 | "id": "0779d76b-afc6-4886-ab65-af8c004273ad",
125 | "createdAt": "2023-01-16T04:01:56.63Z",
126 | "updatedAt": "2023-01-28T05:54:43.583Z",
127 | "envId": "226b9bf8-4af3-4ffa-9b01-162270e4cd40",
128 | "name": "full-segment",
129 | "description": "test segment",
130 | "included": [
131 | "true-1"
132 | ],
133 | "excluded": [
134 | "false-1"
135 | ],
136 | "rules": [
137 | {
138 | "id": "19a77402-3ee7-4ea0-83bc-3e019b982d1b",
139 | "name": "Rule 1",
140 | "conditions": [
141 | {
142 | "property": "age",
143 | "op": "LessEqualThan",
144 | "value": "22"
145 | },
146 | {
147 | "property": "country",
148 | "op": "IsOneOf",
149 | "value": "[\"cn\",\"us\",\"es\"]"
150 | },
151 | {
152 | "property": "name",
153 | "op": "NotEqual",
154 | "value": "bob"
155 | },
156 | {
157 | "property": "isMember",
158 | "op": "IsTrue",
159 | "value": "IsTrue"
160 | }
161 | ]
162 | }
163 | ],
164 | "isArchived": false
165 | }
166 | ]
167 | }
168 | }
--------------------------------------------------------------------------------
/tests/FeatBit.ServerSdk.Tests/Concurrent/AtomicBooleanTests.cs:
--------------------------------------------------------------------------------
1 | namespace FeatBit.Sdk.Server.Concurrent;
2 |
3 | public class AtomicBooleanTests
4 | {
5 | [Fact]
6 | public void SetInitialValue()
7 | {
8 | var trueBoolean = new AtomicBoolean(true);
9 | Assert.True(trueBoolean.Value);
10 |
11 | var falseBoolean = new AtomicBoolean(false);
12 | Assert.False(falseBoolean.Value);
13 | }
14 |
15 | [Fact]
16 | public void CompareAndSetWhenMatch()
17 | {
18 | var atomicBoolean = new AtomicBoolean(true);
19 |
20 | var newValueSet = atomicBoolean.CompareAndSet(true, false);
21 |
22 | Assert.True(newValueSet);
23 | Assert.False(atomicBoolean.Value);
24 | }
25 |
26 | [Fact]
27 | public void CompareAndSetWhenNotMatch()
28 | {
29 | var atomicBoolean = new AtomicBoolean(true);
30 |
31 | var newValueSet = atomicBoolean.CompareAndSet(false, false);
32 |
33 | Assert.False(newValueSet);
34 | Assert.True(atomicBoolean.Value);
35 | }
36 |
37 | [Fact]
38 | public void GetAndSet()
39 | {
40 | var atomicBoolean = new AtomicBoolean(true);
41 |
42 | var oldValue = atomicBoolean.GetAndSet(false);
43 |
44 | Assert.False(atomicBoolean.Value);
45 | Assert.True(oldValue);
46 | }
47 |
48 | [Fact]
49 | public void CastToBool()
50 | {
51 | var atomicBoolean = new AtomicBoolean(true);
52 | bool boolValue = atomicBoolean;
53 | Assert.True(boolValue);
54 | }
55 | }
--------------------------------------------------------------------------------
/tests/FeatBit.ServerSdk.Tests/Concurrent/StatusManagerTests.cs:
--------------------------------------------------------------------------------
1 | namespace FeatBit.Sdk.Server.Concurrent;
2 |
3 | public class StatusManagerTests
4 | {
5 | public enum TestStatus
6 | {
7 | A,
8 | B
9 | }
10 |
11 | [Fact]
12 | public void SetInitialStatus()
13 | {
14 | var statusManager = new StatusManager(TestStatus.A);
15 | Assert.Equal(TestStatus.A, statusManager.Status);
16 | }
17 |
18 | [Fact]
19 | public void ChangesStatus()
20 | {
21 | var statusManager = new StatusManager(TestStatus.A);
22 | statusManager.SetStatus(TestStatus.B);
23 | Assert.Equal(TestStatus.B, statusManager.Status);
24 | }
25 |
26 | [Theory]
27 | [InlineData(TestStatus.A, TestStatus.B, true)]
28 | [InlineData(TestStatus.A, TestStatus.A, false)]
29 | public void TriggerIfStatusChangedEvent(TestStatus old, TestStatus @new, bool changed)
30 | {
31 | var eventTriggered = false;
32 | var statusManager = new StatusManager(old, newStatus =>
33 | {
34 | Assert.Equal(newStatus, @new);
35 | eventTriggered = true;
36 | });
37 | statusManager.SetStatus(@new);
38 |
39 | Assert.Equal(changed, eventTriggered);
40 | }
41 |
42 | [Theory]
43 | [InlineData(TestStatus.A, TestStatus.B, true)]
44 | [InlineData(TestStatus.B, TestStatus.A, false)]
45 | public void CompareAndSetStatus(TestStatus expected, TestStatus newStatus, bool setSuccess)
46 | {
47 | var statusManager = new StatusManager(TestStatus.A);
48 | var compareAndSetSuccess = statusManager.CompareAndSet(expected, newStatus);
49 |
50 | Assert.Equal(setSuccess, compareAndSetSuccess);
51 | Assert.Equal(newStatus, statusManager.Status);
52 | }
53 | }
--------------------------------------------------------------------------------
/tests/FeatBit.ServerSdk.Tests/DataSynchronizer/WebSocketDataSynchronizerTests.cs:
--------------------------------------------------------------------------------
1 | using FeatBit.Sdk.Server.Model;
2 | using FeatBit.Sdk.Server.Options;
3 | using FeatBit.Sdk.Server.Store;
4 |
5 | namespace FeatBit.Sdk.Server.DataSynchronizer;
6 |
7 | [UsesVerify]
8 | [Collection(nameof(TestApp))]
9 | public class WebSocketDataSynchronizerTests
10 | {
11 | private readonly TestApp _app;
12 |
13 | public WebSocketDataSynchronizerTests(TestApp app)
14 | {
15 | _app = app;
16 | }
17 |
18 | [Fact]
19 | public async Task StartWithEmptyStoreAsync()
20 | {
21 | var options = new FbOptionsBuilder("qJHQTVfsZUOu1Q54RLMuIQ-JtrIvNK-k-bARYicOTNQA")
22 | .Streaming(new Uri("ws://localhost/"))
23 | .Build();
24 |
25 | var store = new DefaultMemoryStore();
26 | var synchronizer = new WebSocketDataSynchronizer(options, store, op => _app.CreateFbWebSocket(op));
27 | Assert.Equal(DataSynchronizerStatus.Starting, synchronizer.Status);
28 |
29 | var startTask = synchronizer.StartAsync();
30 | await startTask.WaitAsync(options.StartWaitTime);
31 |
32 | Assert.True(store.Populated);
33 | Assert.True(synchronizer.Initialized);
34 | Assert.Equal(DataSynchronizerStatus.Stable, synchronizer.Status);
35 |
36 | var flag = store.Get("ff_returns-true");
37 | Assert.NotNull(flag);
38 |
39 | var segment = store.Get("segment_0779d76b-afc6-4886-ab65-af8c004273ad");
40 | Assert.NotNull(segment);
41 | }
42 |
43 | [Fact]
44 | public async Task StartWithPopulatedStoreAsync()
45 | {
46 | var options = new FbOptionsBuilder("qJHQTVfsZUOu1Q54RLMuIQ-JtrIvNK-k-bARYicOTNQA")
47 | .Streaming(new Uri("ws://localhost/"))
48 | .Build();
49 |
50 | var store = new DefaultMemoryStore();
51 | var hello = new FeatureFlagBuilder().Key("hello-world").Version(1).Build();
52 | store.Populate(new[] { hello });
53 |
54 | var synchronizer = new WebSocketDataSynchronizer(options, store, op => _app.CreateFbWebSocket(op));
55 | Assert.Equal(DataSynchronizerStatus.Starting, synchronizer.Status);
56 |
57 | var startTask = synchronizer.StartAsync();
58 | await startTask.WaitAsync(options.StartWaitTime);
59 |
60 | Assert.True(synchronizer.Initialized);
61 | Assert.Equal(DataSynchronizerStatus.Stable, synchronizer.Status);
62 |
63 | var flag = store.Get("ff_returns-true");
64 | Assert.NotNull(flag);
65 | }
66 |
67 | [Fact]
68 | public async Task ServerRejectConnection()
69 | {
70 | var options = new FbOptionsBuilder().Build();
71 | var store = new DefaultMemoryStore();
72 |
73 | var synchronizer =
74 | new WebSocketDataSynchronizer(options, store, _ => _app.CreateFbWebSocket("close-with-4003"));
75 | Assert.Equal(DataSynchronizerStatus.Starting, synchronizer.Status);
76 |
77 | _ = synchronizer.StartAsync();
78 |
79 | var tcs = new TaskCompletionSource();
80 | var onStatusChangedTask = tcs.Task;
81 | synchronizer.StatusChanged += _ =>
82 | {
83 | Assert.False(synchronizer.Initialized);
84 | Assert.Equal(DataSynchronizerStatus.Stopped, synchronizer.Status);
85 | tcs.SetResult();
86 | };
87 | await onStatusChangedTask.WaitAsync(TimeSpan.FromSeconds(1));
88 | }
89 |
90 | [Fact]
91 | public async Task ServerDisconnectedAfterStable()
92 | {
93 | var options = new FbOptionsBuilder()
94 | .ReconnectRetryDelays(new[] { TimeSpan.FromMilliseconds(200) })
95 | .Build();
96 | var store = new DefaultMemoryStore();
97 |
98 | var webSocketUri = new Uri("ws://localhost/streaming?type=server&token=close-after-first-datasync");
99 | var synchronizer =
100 | new WebSocketDataSynchronizer(options, store, op => _app.CreateFbWebSocket(op, webSocketUri));
101 | Assert.Equal(DataSynchronizerStatus.Starting, synchronizer.Status);
102 |
103 | var startTask = synchronizer.StartAsync();
104 | await startTask.WaitAsync(options.StartWaitTime);
105 |
106 | Assert.True(synchronizer.Initialized);
107 | Assert.Equal(DataSynchronizerStatus.Stable, synchronizer.Status);
108 |
109 | var tcs = new TaskCompletionSource();
110 | var onStatusChangedTask = tcs.Task;
111 | synchronizer.StatusChanged += _ =>
112 | {
113 | Assert.Equal(DataSynchronizerStatus.Interrupted, synchronizer.Status);
114 | tcs.SetResult();
115 | };
116 | await onStatusChangedTask.WaitAsync(TimeSpan.FromSeconds(1));
117 | }
118 | }
--------------------------------------------------------------------------------
/tests/FeatBit.ServerSdk.Tests/DataSynchronizer/full-data-set.json:
--------------------------------------------------------------------------------
1 | {
2 | "messageType": "data-sync",
3 | "data": {
4 | "eventType": "full",
5 | "featureFlags": [
6 | {
7 | "id": "174c7138-426d-4434-8a91-af8c00306de3",
8 | "createdAt": "2023-01-16T02:56:19.531Z",
9 | "updatedAt": "2023-01-28T02:04:55.616Z",
10 | "creatorId": "197dea2a-1044-497d-8f1e-f01d2e15a756",
11 | "updatorId": "197dea2a-1044-497d-8f1e-f01d2e15a756",
12 | "envId": "226b9bf8-4af3-4ffa-9b01-162270e4cd40",
13 | "name": "returns true",
14 | "key": "returns-true",
15 | "variationType": "boolean",
16 | "variations": [
17 | {
18 | "id": "3da96792-debf-4878-905a-c9b5f9178cd0",
19 | "value": "true"
20 | },
21 | {
22 | "id": "19cf9fb6-b790-4877-ab96-b5c5e73e0b46",
23 | "value": "false"
24 | }
25 | ],
26 | "targetUsers": [],
27 | "rules": [],
28 | "isEnabled": true,
29 | "disabledVariationId": "19cf9fb6-b790-4877-ab96-b5c5e73e0b46",
30 | "fallthrough": {
31 | "dispatchKey": null,
32 | "includedInExpt": true,
33 | "variations": [
34 | {
35 | "id": "3da96792-debf-4878-905a-c9b5f9178cd0",
36 | "rollout": [
37 | 0,
38 | 1
39 | ],
40 | "exptRollout": 1
41 | }
42 | ]
43 | },
44 | "exptIncludeAllTargets": true,
45 | "tags": [],
46 | "isArchived": false
47 | }
48 | ],
49 | "segments": [
50 | {
51 | "id": "0779d76b-afc6-4886-ab65-af8c004273ad",
52 | "createdAt": "2023-01-16T04:01:56.63Z",
53 | "updatedAt": "2023-01-28T05:54:43.583Z",
54 | "envId": "226b9bf8-4af3-4ffa-9b01-162270e4cd40",
55 | "name": "full-segment",
56 | "description": "test segment",
57 | "included": [
58 | "true-1"
59 | ],
60 | "excluded": [
61 | "false-1"
62 | ],
63 | "rules": [
64 | {
65 | "id": "19a77402-3ee7-4ea0-83bc-3e019b982d1b",
66 | "name": "Rule 1",
67 | "conditions": [
68 | {
69 | "property": "age",
70 | "op": "LessEqualThan",
71 | "value": "22"
72 | },
73 | {
74 | "property": "country",
75 | "op": "IsOneOf",
76 | "value": "[\"cn\",\"us\",\"es\"]"
77 | },
78 | {
79 | "property": "name",
80 | "op": "NotEqual",
81 | "value": "bob"
82 | },
83 | {
84 | "property": "isMember",
85 | "op": "IsTrue",
86 | "value": "IsTrue"
87 | }
88 | ]
89 | }
90 | ],
91 | "isArchived": false
92 | }
93 | ]
94 | }
95 | }
--------------------------------------------------------------------------------
/tests/FeatBit.ServerSdk.Tests/DataSynchronizer/patch-data-set.json:
--------------------------------------------------------------------------------
1 | {
2 | "messageType": "data-sync",
3 | "data": {
4 | "eventType": "patch",
5 | "featureFlags": [
6 | {
7 | "id": "174c7138-426d-4434-8a91-af8c00306de3",
8 | "createdAt": "2023-01-16T02:56:19.531Z",
9 | "updatedAt": "2023-02-01T02:04:55.616Z",
10 | "creatorId": "197dea2a-1044-497d-8f1e-f01d2e15a756",
11 | "updatorId": "197dea2a-1044-497d-8f1e-f01d2e15a756",
12 | "envId": "226b9bf8-4af3-4ffa-9b01-162270e4cd40",
13 | "name": "[updated] returns true",
14 | "key": "returns-true",
15 | "variationType": "boolean",
16 | "variations": [
17 | {
18 | "id": "3da96792-debf-4878-905a-c9b5f9178cd0",
19 | "value": "true"
20 | },
21 | {
22 | "id": "19cf9fb6-b790-4877-ab96-b5c5e73e0b46",
23 | "value": "false"
24 | }
25 | ],
26 | "targetUsers": [],
27 | "rules": [],
28 | "isEnabled": true,
29 | "disabledVariationId": "19cf9fb6-b790-4877-ab96-b5c5e73e0b46",
30 | "fallthrough": {
31 | "dispatchKey": null,
32 | "includedInExpt": true,
33 | "variations": [
34 | {
35 | "id": "3da96792-debf-4878-905a-c9b5f9178cd0",
36 | "rollout": [
37 | 0,
38 | 1
39 | ],
40 | "exptRollout": 1
41 | }
42 | ]
43 | },
44 | "exptIncludeAllTargets": true,
45 | "tags": [],
46 | "isArchived": false
47 | }
48 | ],
49 | "segments": []
50 | }
51 | }
--------------------------------------------------------------------------------
/tests/FeatBit.ServerSdk.Tests/Evaluation/ConditionMatcherTests.cs:
--------------------------------------------------------------------------------
1 | using FeatBit.Sdk.Server.Model;
2 |
3 | namespace FeatBit.Sdk.Server.Evaluation;
4 |
5 | public class ConditionMatcherTests
6 | {
7 | // note:
8 | // uv = userValue
9 | // op = operation
10 | // rv = ruleValue
11 |
12 | [Theory]
13 | [InlineData("10", OperatorTypes.BiggerThan, "9", true)]
14 | [InlineData("10", OperatorTypes.BiggerThan, "11", false)]
15 | [InlineData("10", OperatorTypes.BiggerEqualThan, "10", true)]
16 | [InlineData("10", OperatorTypes.BiggerEqualThan, "11", false)]
17 | [InlineData("10", OperatorTypes.LessThan, "11", true)]
18 | [InlineData("10", OperatorTypes.LessThan, "9", false)]
19 | [InlineData("10", OperatorTypes.LessEqualThan, "10", true)]
20 | [InlineData("10", OperatorTypes.LessEqualThan, "9", false)]
21 | public void MatchNumeric(string uv, string op, string rv, bool expected)
22 | {
23 | CheckMatch(uv, op, rv, expected);
24 | }
25 |
26 | [Theory]
27 | [InlineData("v1.0.0", OperatorTypes.Equal, "v1.0.0", true)]
28 | [InlineData("v1.1.0", OperatorTypes.Equal, "v1.0.0", false)]
29 | [InlineData("v1.1.0", OperatorTypes.NotEqual, "v1.1.0", false)]
30 | [InlineData("v1.1.0", OperatorTypes.NotEqual, "v1.0.0", true)]
31 | public void MatchEquality(string uv, string op, string rv, bool expected)
32 | {
33 | CheckMatch(uv, op, rv, expected);
34 | }
35 |
36 | [Theory]
37 | [InlineData("vvip", OperatorTypes.Contains, "vip", true)]
38 | [InlineData("vvip", OperatorTypes.Contains, "sv", false)]
39 | [InlineData("svip", OperatorTypes.NotContain, "vv", true)]
40 | [InlineData("svip", OperatorTypes.NotContain, "vip", false)]
41 | public void MatchContainsOrNot(string uv, string op, string rv, bool expected)
42 | {
43 | CheckMatch(uv, op, rv, expected);
44 | }
45 |
46 | [Theory]
47 | [InlineData("abc", OperatorTypes.StartsWith, "ab", true)]
48 | [InlineData("abc", OperatorTypes.StartsWith, "b", false)]
49 | [InlineData("abc", OperatorTypes.EndsWith, "bc", true)]
50 | [InlineData("abc", OperatorTypes.EndsWith, "cd", false)]
51 | public void MatchStartsOrEndsWith(string uv, string op, string rv, bool expected)
52 | {
53 | CheckMatch(uv, op, rv, expected);
54 | }
55 |
56 | [Theory]
57 | [InlineData("color", OperatorTypes.MatchRegex, "colou?r", true)]
58 | [InlineData("colour", OperatorTypes.MatchRegex, "colorr?", false)]
59 | [InlineData("colouur", OperatorTypes.NotMatchRegex, "colou?r", true)]
60 | [InlineData("color", OperatorTypes.NotMatchRegex, "colou?r", false)]
61 | public void MatchRegexOrNot(string uv, string op, string rv, bool expected)
62 | {
63 | CheckMatch(uv, op, rv, expected);
64 | }
65 |
66 | [Theory]
67 | [InlineData("a", OperatorTypes.IsOneOf, "[\"a\", \"b\"]", true)]
68 | [InlineData("c", OperatorTypes.IsOneOf, "[\"a\", \"b\"]", false)]
69 | [InlineData("c", OperatorTypes.NotOneOf, "[\"a\", \"b\"]", true)]
70 | [InlineData("a", OperatorTypes.NotOneOf, "[\"a\", \"b\"]", false)]
71 | public void MatchIsOneOf(string uv, string op, string rv, bool expected)
72 | {
73 | CheckMatch(uv, op, rv, expected);
74 | }
75 |
76 | [Theory]
77 | [InlineData("true", OperatorTypes.IsTrue, "", true)]
78 | [InlineData("TRue", OperatorTypes.IsTrue, "", true)]
79 | [InlineData("false", OperatorTypes.IsFalse, "", true)]
80 | [InlineData("falSE", OperatorTypes.IsFalse, "", true)]
81 | [InlineData("not-true-string", OperatorTypes.IsTrue, "", false)]
82 | [InlineData("not-false-string", OperatorTypes.IsFalse, "", false)]
83 | public void MatchTrueFalse(string uv, string op, string rv, bool expected)
84 | {
85 | CheckMatch(uv, op, rv, expected);
86 | }
87 |
88 | private static void CheckMatch(string uv, string op, string rv, bool expected)
89 | {
90 | var condition = new Condition
91 | {
92 | Property = "prop",
93 | Op = op,
94 | Value = rv
95 | };
96 |
97 | var user = FbUser.Builder("nope")
98 | .Custom("prop", uv)
99 | .Build();
100 |
101 | var isMatch = Evaluator.IsMatchCondition(condition, user);
102 | Assert.Equal(expected, isMatch);
103 | }
104 | }
--------------------------------------------------------------------------------
/tests/FeatBit.ServerSdk.Tests/Evaluation/DispatchAlgorithmTests.cs:
--------------------------------------------------------------------------------
1 | namespace FeatBit.Sdk.Server.Evaluation;
2 |
3 | public class DispatchAlgorithmTests
4 | {
5 | [Theory]
6 | [InlineData("test-value", 0.14653629204258323)]
7 | [InlineData("qKPKh1S3FolC", 0.9105919692665339)]
8 | [InlineData("3eacb184-2d79-49df-9ea7-edd4f10e4c6f", 0.08994403155520558)]
9 | public void ReturnSameResultForAnGivenKey(string key, double value)
10 | {
11 | Assert.Equal(value, DispatchAlgorithm.RolloutOfKey(key));
12 | }
13 | }
--------------------------------------------------------------------------------
/tests/FeatBit.ServerSdk.Tests/Evaluation/RuleMatcherTests.cs:
--------------------------------------------------------------------------------
1 | using FeatBit.Sdk.Server.Model;
2 | using FeatBit.Sdk.Server.Store;
3 |
4 | namespace FeatBit.Sdk.Server.Evaluation;
5 |
6 | public class RuleMatcherTests
7 | {
8 | [Fact]
9 | public void MatchRule()
10 | {
11 | var segmentId = Guid.NewGuid();
12 | var segment = new SegmentBuilder()
13 | .Id(segmentId)
14 | .Included("u1")
15 | .Excluded("u2", "u3")
16 | .Build();
17 |
18 | var store = new DefaultMemoryStore();
19 | store.Populate(new[] { segment });
20 |
21 | var evaluator = new Evaluator(store);
22 |
23 | var rule = new TargetRule
24 | {
25 | Name = "test",
26 | Conditions = new List
27 | {
28 | new()
29 | {
30 | Property = Evaluator.IsNotInSegmentProperty,
31 | Op = string.Empty,
32 | Value = $"[\"{segmentId}\"]"
33 | },
34 | new()
35 | {
36 | Property = "age",
37 | Op = OperatorTypes.Equal,
38 | Value = "10"
39 | }
40 | }
41 | };
42 |
43 | var u1 = new FbUserBuilder("u1").Build();
44 | var u2 = new FbUserBuilder("u2")
45 | .Custom("age", "11")
46 | .Build();
47 | var u3 = new FbUserBuilder("u3")
48 | .Custom("age", "10")
49 | .Build();
50 |
51 | Assert.False(evaluator.IsMatchRule(rule, u1));
52 | Assert.False(evaluator.IsMatchRule(rule, u2));
53 | Assert.True(evaluator.IsMatchRule(rule, u3));
54 | }
55 | }
--------------------------------------------------------------------------------
/tests/FeatBit.ServerSdk.Tests/Evaluation/SegmentMatcherTests.cs:
--------------------------------------------------------------------------------
1 | using FeatBit.Sdk.Server.Model;
2 | using FeatBit.Sdk.Server.Store;
3 |
4 | namespace FeatBit.Sdk.Server.Evaluation;
5 |
6 | public class SegmentMatcherTests
7 | {
8 | [Fact]
9 | public void MatchSegment()
10 | {
11 | var rule = new MatchRule
12 | {
13 | Conditions = new List
14 | {
15 | new()
16 | {
17 | Property = "age",
18 | Op = OperatorTypes.Equal,
19 | Value = "10"
20 | },
21 | new()
22 | {
23 | Property = "country",
24 | Op = OperatorTypes.Equal,
25 | Value = "us"
26 | },
27 | }
28 | };
29 |
30 | var segment = new SegmentBuilder()
31 | .Included("u1")
32 | .Excluded("u2")
33 | .Rules(rule)
34 | .Build();
35 |
36 | var u1 = FbUser.Builder("u1").Build();
37 | var u2 = FbUser.Builder("u2").Build();
38 |
39 | var u3 = FbUser.Builder("u3")
40 | .Custom("age", "10")
41 | .Custom("country", "us")
42 | .Build();
43 |
44 | var u4 = FbUser.Builder("u4")
45 | .Custom("age", "10")
46 | .Custom("country", "eu")
47 | .Build();
48 |
49 | Assert.True(Evaluator.IsMatchSegment(segment, u1));
50 | Assert.False(Evaluator.IsMatchSegment(segment, u2));
51 | Assert.True(Evaluator.IsMatchSegment(segment, u3));
52 | Assert.False(Evaluator.IsMatchSegment(segment, u4));
53 | }
54 |
55 | [Fact]
56 | public void MatchAnySegment()
57 | {
58 | var store = new DefaultMemoryStore();
59 |
60 | var segmentId = Guid.NewGuid();
61 | var segment = new SegmentBuilder()
62 | .Id(segmentId)
63 | .Included("u1")
64 | .Build();
65 | store.Populate(new[] { segment });
66 |
67 | var evaluator = new Evaluator(store);
68 |
69 | var segmentCondition = new Condition
70 | {
71 | Property = Evaluator.IsInSegmentProperty,
72 | Op = string.Empty,
73 | Value = $"[\"{segmentId}\"]",
74 | };
75 | var user = FbUser.Builder("u1").Build();
76 |
77 | Assert.True(evaluator.IsMatchAnySegment(segmentCondition, user));
78 | }
79 | }
--------------------------------------------------------------------------------
/tests/FeatBit.ServerSdk.Tests/Events/AsyncEventTests.cs:
--------------------------------------------------------------------------------
1 | namespace FeatBit.Sdk.Server.Events;
2 |
3 | public class AsyncEventTests
4 | {
5 | [Theory]
6 | [ClassData(typeof(AsyncEvents))]
7 | internal void AsyncEventIsCompleted(AsyncEvent asyncEvent)
8 | {
9 | Assert.False(asyncEvent.IsCompleted);
10 |
11 | asyncEvent.Complete();
12 |
13 | Assert.True(asyncEvent.IsCompleted);
14 | }
15 |
16 | [Fact]
17 | internal void WaitForCompletion()
18 | {
19 | Assert.True(CompleteInTime(new FlushEvent(), 100, 300));
20 | Assert.False(CompleteInTime(new ShutdownEvent(), 300, 100));
21 | }
22 |
23 | [Fact]
24 | internal async Task WaitForCompletionAsync()
25 | {
26 | Assert.True(await CompleteInTimeAsync(new FlushEvent(), 100, 300));
27 | Assert.False(await CompleteInTimeAsync(new ShutdownEvent(), 300, 100));
28 | }
29 |
30 | private static bool CompleteInTime(AsyncEvent asyncEvent, int timeToComplete, int timeout)
31 | {
32 | _ = Task.Delay(timeToComplete).ContinueWith(_ => asyncEvent.Complete());
33 | return asyncEvent.WaitForCompletion(TimeSpan.FromMilliseconds(timeout));
34 | }
35 |
36 | private static async Task CompleteInTimeAsync(AsyncEvent asyncEvent, int timeToComplete, int timeout)
37 | {
38 | _ = Task.Delay(timeToComplete).ContinueWith(_ => asyncEvent.Complete());
39 | return await asyncEvent.WaitForCompletionAsync(TimeSpan.FromMilliseconds(timeout));
40 | }
41 | }
42 |
43 | internal class AsyncEvents : TheoryData
44 | {
45 | public AsyncEvents()
46 | {
47 | Add(new FlushEvent());
48 | Add(new ShutdownEvent());
49 | }
50 | }
--------------------------------------------------------------------------------
/tests/FeatBit.ServerSdk.Tests/Events/DefaultEventBufferTests.cs:
--------------------------------------------------------------------------------
1 | namespace FeatBit.Sdk.Server.Events;
2 |
3 | public class DefaultEventBufferTests
4 | {
5 | [Fact]
6 | public void GetEventSnapshot()
7 | {
8 | var buffer = new DefaultEventBuffer(2);
9 |
10 | buffer.AddEvent(new IntEvent(1));
11 | buffer.AddEvent(new IntEvent(2));
12 |
13 | var snapshot = buffer.EventsSnapshot;
14 |
15 | buffer.Clear();
16 | Assert.Equal(0, buffer.Count);
17 | Assert.True(buffer.IsEmpty);
18 |
19 | // after the buffer is cleared, the snapshot should remain unchanged
20 | Assert.Equal(2, snapshot.Length);
21 |
22 | Assert.Equal(1, ((IntEvent)snapshot[0]).Value);
23 | Assert.Equal(2, ((IntEvent)snapshot[1]).Value);
24 | }
25 |
26 | [Fact]
27 | public void IgnoreNewEventAfterBufferIsFull()
28 | {
29 | var buffer = new DefaultEventBuffer(2);
30 |
31 | Assert.True(buffer.AddEvent(new IntEvent(1)));
32 | Assert.True(buffer.AddEvent(new IntEvent(2)));
33 |
34 | // buffer is full, the following events should be ignored
35 | Assert.False(buffer.AddEvent(new IntEvent(3)));
36 | Assert.False(buffer.AddEvent(new IntEvent(4)));
37 |
38 | Assert.Equal(2, buffer.Count);
39 | }
40 | }
--------------------------------------------------------------------------------
/tests/FeatBit.ServerSdk.Tests/Events/DefaultEventDispatcherTests.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Concurrent;
2 | using FeatBit.Sdk.Server.Options;
3 |
4 | namespace FeatBit.Sdk.Server.Events;
5 |
6 | public class DefaultEventDispatcherTests
7 | {
8 | [Fact]
9 | public void NewAndDispose()
10 | {
11 | var queue = new BlockingCollection();
12 | var options = new FbOptionsBuilder("fake-secret").Build();
13 |
14 | using var dispatcher = new DefaultEventDispatcher(options, queue);
15 | }
16 |
17 | [Fact]
18 | public void AddEvent()
19 | {
20 | var queue = new BlockingCollection();
21 | var options = new FbOptionsBuilder("fake-secret").Build();
22 |
23 | var mockBuffer = new Mock();
24 |
25 | using var dispatcher = new DefaultEventDispatcher(options, queue, buffer: mockBuffer.Object);
26 |
27 | queue.Add(new IntEvent(1));
28 | queue.Add(new IntEvent(2));
29 |
30 | Assert.True(SpinWait.SpinUntil(() => mockBuffer.Invocations.Count > 0, 1000));
31 | mockBuffer.Verify(x => x.AddEvent(It.IsAny()), Times.Exactly(2));
32 | }
33 |
34 | [Fact]
35 | public void Flush()
36 | {
37 | var queue = new BlockingCollection();
38 | var options = new FbOptionsBuilder("fake-secret").Build();
39 |
40 | var mockSender = new Mock();
41 |
42 | using var dispatcher =
43 | new DefaultEventDispatcher(options, queue, sender: mockSender.Object);
44 |
45 | queue.Add(new IntEvent(1));
46 | queue.Add(new IntEvent(2));
47 |
48 | FlushAndWaitComplete(queue);
49 |
50 | mockSender.Verify(x => x.SendAsync(It.IsAny()), Times.Once);
51 | }
52 |
53 | [Fact]
54 | public void FlushEmptyBuffer()
55 | {
56 | var queue = new BlockingCollection();
57 | var options = new FbOptionsBuilder("fake-secret").Build();
58 |
59 | using var dispatcher = new DefaultEventDispatcher(options, queue);
60 |
61 | FlushAndWaitComplete(queue);
62 | }
63 |
64 | [Fact]
65 | public void Shutdown()
66 | {
67 | var queue = new BlockingCollection();
68 | var options = new FbOptionsBuilder("fake-secret").Build();
69 |
70 | using var dispatcher = new DefaultEventDispatcher(options, queue);
71 |
72 | var shutdownEvent = new ShutdownEvent();
73 | queue.Add(shutdownEvent);
74 | EnsureAsyncEventComplete(shutdownEvent);
75 | }
76 |
77 | [Fact]
78 | public void EventSenderStopDispatcher()
79 | {
80 | var queue = new BlockingCollection();
81 | var options = new FbOptionsBuilder("fake-secret").Build();
82 |
83 | var mockSender = new Mock();
84 | mockSender.Setup(x => x.SendAsync(It.IsAny()))
85 | .Returns(Task.FromResult(DeliveryStatus.FailedAndMustShutDown));
86 |
87 | // Create a dispatcher with a mock sender that always fails
88 | using var dispatcher = new DefaultEventDispatcher(options, queue, sender: mockSender.Object);
89 |
90 | // Add an event and flush it to trigger the sender
91 | AddEventThenFlush();
92 | mockSender.Verify(x => x.SendAsync(It.IsAny()), Times.Once);
93 |
94 | // Clear previous invocations for later verification
95 | mockSender.Invocations.Clear();
96 |
97 | // Check if dispatcher stopped after event sender return FailedAndMustShutDown
98 | Assert.True(dispatcher.HasStopped);
99 |
100 | // Check if add and flush operations are no-op after dispatcher has stopped
101 | AddEventThenFlush();
102 | mockSender.Verify(x => x.SendAsync(It.IsAny()), Times.Never);
103 |
104 | void AddEventThenFlush()
105 | {
106 | queue.Add(new IntEvent(1));
107 | FlushAndWaitComplete(queue);
108 | }
109 | }
110 |
111 | [Theory]
112 | [InlineData(5, 3, 1)]
113 | [InlineData(3, 12, 4)]
114 | [InlineData(5, 12, 3)]
115 | public void SendEventsInMultiBatch(int eventPerRequest, int totalEvents, int expectedBatch)
116 | {
117 | var queue = new BlockingCollection();
118 | var options = new FbOptionsBuilder("fake-secret")
119 | .MaxEventPerRequest(eventPerRequest)
120 | .Build();
121 |
122 | var mockSender = new Mock();
123 |
124 | using var dispatcher = new DefaultEventDispatcher(options, queue, sender: mockSender.Object);
125 |
126 | for (var i = 0; i < totalEvents; i++)
127 | {
128 | queue.Add(new IntEvent(i));
129 | }
130 |
131 | FlushAndWaitComplete(queue);
132 | mockSender.Verify(x => x.SendAsync(It.IsAny()), Times.Exactly(expectedBatch));
133 | }
134 |
135 | private static void FlushAndWaitComplete(BlockingCollection queue)
136 | {
137 | var flushEvent = new FlushEvent();
138 | queue.Add(flushEvent);
139 | EnsureAsyncEventComplete(flushEvent);
140 | }
141 |
142 | private static void EnsureAsyncEventComplete(AsyncEvent asyncEvent)
143 | {
144 | Assert.True(SpinWait.SpinUntil(() => asyncEvent.IsCompleted, 1000));
145 | }
146 | }
--------------------------------------------------------------------------------
/tests/FeatBit.ServerSdk.Tests/Events/DefaultEventProcessorTests.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Concurrent;
2 | using FeatBit.Sdk.Server.Options;
3 | using Microsoft.Extensions.Logging;
4 |
5 | namespace FeatBit.Sdk.Server.Events;
6 |
7 | public class DefaultEventProcessorTests
8 | {
9 | [Fact]
10 | public void StartAndClose()
11 | {
12 | var options = new FbOptionsBuilder("secret").Build();
13 | var processor = new DefaultEventProcessor(options);
14 |
15 | // this should complete immediately
16 | processor.FlushAndClose(TimeSpan.FromMilliseconds(1000));
17 |
18 | Assert.True(processor.HasClosed);
19 | }
20 |
21 | [Fact]
22 | public void CloseAnProcessorMultiTimes()
23 | {
24 | var options = new FbOptionsBuilder("secret").Build();
25 | var processor = new DefaultEventProcessor(options);
26 |
27 | processor.FlushAndClose(TimeSpan.FromMilliseconds(1000));
28 | processor.FlushAndClose(TimeSpan.FromMilliseconds(1000));
29 | }
30 |
31 | [Fact]
32 | public void RecordEvent()
33 | {
34 | var options = new FbOptionsBuilder("secret").Build();
35 | var processor = new DefaultEventProcessor(options);
36 |
37 | Assert.True(processor.Record(new IntEvent(1)));
38 | }
39 |
40 | [Fact]
41 | public void RecordNullEvent()
42 | {
43 | var options = new FbOptionsBuilder("secret").Build();
44 | var processor = new DefaultEventProcessor(options);
45 |
46 | Assert.False(processor.Record(null));
47 | }
48 |
49 | [Fact]
50 | public void ExceedCapacity()
51 | {
52 | var loggerMock = new Mock>();
53 |
54 | var options = new FbOptionsBuilder("secret")
55 | // set max queue size to 2
56 | .MaxEventsInQueue(2)
57 | .Build();
58 |
59 | // create a dispatcher that will not consume the processor's message so that processor's queue can be full
60 | var dispatcher = new DefaultEventDispatcher(options, new BlockingCollection());
61 | var processor = new DefaultEventProcessor(options, loggerMock.Object, (_, _) => dispatcher);
62 |
63 | Assert.True(processor.Record(new IntEvent(1)));
64 | Assert.True(processor.Record(new IntEvent(2)));
65 |
66 | // the processor rejects new events when its queue is full
67 | Assert.False(processor.Record(new IntEvent(3)));
68 |
69 | // the processor will directly complete any flush event that is rejected.
70 | var flushEvent = new FlushEvent();
71 | Assert.False(processor.Record(flushEvent));
72 | Assert.True(flushEvent.IsCompleted);
73 |
74 | // verify warning logged once
75 | loggerMock.Verify(
76 | logger => logger.Log(
77 | LogLevel.Warning,
78 | It.IsAny(),
79 | It.IsAny(),
80 | null,
81 | It.IsAny>()
82 | ),
83 | Times.Once
84 | );
85 | }
86 |
87 | [Fact]
88 | public void WaitFlush()
89 | {
90 | var options = new FbOptionsBuilder("secret").Build();
91 | var mockedSender = new Mock();
92 |
93 | var processor = new DefaultEventProcessor(
94 | options,
95 | dispatcherFactory: (opts, queue) => new DefaultEventDispatcher(opts, queue, sender: mockedSender.Object)
96 | );
97 |
98 | processor.Record(new IntEvent(1));
99 | var flushedInTime = processor.FlushAndWait(TimeSpan.FromMilliseconds(1000));
100 |
101 | Assert.True(flushedInTime);
102 | mockedSender.Verify(x => x.SendAsync(It.IsAny()), Times.Once);
103 | }
104 |
105 | [Fact]
106 | public async Task WaitFlushAsync()
107 | {
108 | var options = new FbOptionsBuilder("secret").Build();
109 | var mockedSender = new Mock();
110 |
111 | var processor = new DefaultEventProcessor(
112 | options,
113 | dispatcherFactory: (opts, queue) => new DefaultEventDispatcher(opts, queue, sender: mockedSender.Object)
114 | );
115 |
116 | processor.Record(new IntEvent(1));
117 | var flushedInTime = await processor.FlushAndWaitAsync(TimeSpan.FromMilliseconds(1000));
118 |
119 | Assert.True(flushedInTime);
120 | mockedSender.Verify(x => x.SendAsync(It.IsAny()), Times.Once);
121 | }
122 | }
--------------------------------------------------------------------------------
/tests/FeatBit.ServerSdk.Tests/Events/DefaultEventSenderTests.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 | using System.Text;
3 | using FeatBit.Sdk.Server.Options;
4 |
5 | namespace FeatBit.Sdk.Server.Events;
6 |
7 | public class DefaultEventSenderTests
8 | {
9 | [Theory]
10 | // ok
11 | [InlineData(HttpStatusCode.OK, DeliveryStatus.Succeeded)]
12 |
13 | // recoverable error
14 | [InlineData(HttpStatusCode.TooManyRequests, DeliveryStatus.Failed)]
15 | [InlineData(HttpStatusCode.RequestTimeout, DeliveryStatus.Failed)]
16 |
17 | // unrecoverable error
18 | [InlineData(HttpStatusCode.NotFound, DeliveryStatus.FailedAndMustShutDown)]
19 | [InlineData(HttpStatusCode.Unauthorized, DeliveryStatus.FailedAndMustShutDown)]
20 | internal async Task CheckDeliveryStatusBasedOnServerReturns(HttpStatusCode code, DeliveryStatus status)
21 | {
22 | var options = new FbOptionsBuilder("secret").Build();
23 |
24 | var httpClient = new HttpClient(new EventHttpMessageHandlerMock(SequencedCode));
25 | var sender = new DefaultEventSender(options, httpClient);
26 |
27 | var payload = Encoding.UTF8.GetBytes("{ \"value\": 1 }");
28 | var deliveryStatus = await sender.SendAsync(payload);
29 |
30 | Assert.Equal(status, deliveryStatus);
31 |
32 | HttpStatusCode SequencedCode(int _) => code;
33 | }
34 |
35 | [Fact]
36 | public async Task ReturnsOkAfterRetry()
37 | {
38 | var options = new FbOptionsBuilder("secret")
39 | .SendEventRetryInterval(TimeSpan.FromMilliseconds(5))
40 | .Build();
41 |
42 | var httpClient = new HttpClient(new EventHttpMessageHandlerMock(SequencedCode));
43 | var sender = new DefaultEventSender(options, httpClient);
44 |
45 | var payload = Encoding.UTF8.GetBytes("{ \"value\": 1 }");
46 | var deliveryStatus = await sender.SendAsync(payload);
47 |
48 | Assert.Equal(DeliveryStatus.Succeeded, deliveryStatus);
49 |
50 | HttpStatusCode SequencedCode(int sequence) =>
51 | sequence % 2 == 0 ? HttpStatusCode.RequestTimeout : HttpStatusCode.OK;
52 | }
53 | }
54 |
55 | internal sealed class EventHttpMessageHandlerMock : HttpMessageHandler
56 | {
57 | private int _sequence;
58 | private readonly Func _sequencedCode;
59 |
60 | public EventHttpMessageHandlerMock(Func sequencedCode)
61 | {
62 | _sequencedCode = sequencedCode;
63 | _sequence = 0;
64 | }
65 |
66 | protected override Task SendAsync(HttpRequestMessage request,
67 | CancellationToken cancellationToken)
68 | {
69 | Assert.Equal("secret", request.Headers.Authorization?.Scheme);
70 | Assert.Equal(HttpMethod.Post, request.Method);
71 |
72 | var code = _sequencedCode(_sequence++);
73 |
74 | return Task.FromResult(new HttpResponseMessage(code));
75 | }
76 | }
--------------------------------------------------------------------------------
/tests/FeatBit.ServerSdk.Tests/Events/DefaultEventSerializerTests.SerializeCombinedEvents.verified.txt:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | user: {
4 | keyId: u1-Id,
5 | name: u1-name,
6 | customizedProperties: [
7 | {
8 | name: custom,
9 | value: value
10 | },
11 | {
12 | name: country,
13 | value: us
14 | }
15 | ]
16 | },
17 | variations: [
18 | {
19 | featureFlagKey: hello,
20 | variation: {
21 | id: v1Id,
22 | value: v1
23 | },
24 | timestamp: {Scrubbed},
25 | sendToExperiment: true
26 | }
27 | ]
28 | },
29 | {
30 | user: {
31 | keyId: u1-Id,
32 | name: u1-name,
33 | customizedProperties: [
34 | {
35 | name: custom,
36 | value: value
37 | },
38 | {
39 | name: country,
40 | value: us
41 | }
42 | ]
43 | },
44 | metrics: [
45 | {
46 | appType: dotnet-server-side,
47 | route: index/metric,
48 | type: CustomEvent,
49 | eventName: click-button,
50 | numericValue: 1.5,
51 | timestamp: {Scrubbed}
52 | }
53 | ]
54 | }
55 | ]
--------------------------------------------------------------------------------
/tests/FeatBit.ServerSdk.Tests/Events/DefaultEventSerializerTests.SerializeEvalEvent.verified.txt:
--------------------------------------------------------------------------------
1 | {
2 | user: {
3 | keyId: u1-Id,
4 | name: u1-name,
5 | customizedProperties: [
6 | {
7 | name: custom,
8 | value: value
9 | },
10 | {
11 | name: country,
12 | value: us
13 | }
14 | ]
15 | },
16 | variations: [
17 | {
18 | featureFlagKey: hello,
19 | variation: {
20 | id: v1Id,
21 | value: v1
22 | },
23 | timestamp: {Scrubbed},
24 | sendToExperiment: true
25 | }
26 | ]
27 | }
--------------------------------------------------------------------------------
/tests/FeatBit.ServerSdk.Tests/Events/DefaultEventSerializerTests.SerializeEvalEvents.verified.txt:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | user: {
4 | keyId: u2-Id,
5 | name: u2-name,
6 | customizedProperties: [
7 | {
8 | name: age,
9 | value: 10
10 | }
11 | ]
12 | },
13 | variations: [
14 | {
15 | featureFlagKey: hello,
16 | variation: {
17 | id: v2Id,
18 | value: v2
19 | },
20 | timestamp: {Scrubbed},
21 | sendToExperiment: false
22 | }
23 | ]
24 | },
25 | {
26 | user: {
27 | keyId: u3-Id,
28 | name: u3-name,
29 | customizedProperties: [
30 | {
31 | name: age,
32 | value: 10
33 | }
34 | ]
35 | },
36 | variations: [
37 | {
38 | featureFlagKey: hello,
39 | variation: {
40 | id: v3Id,
41 | value: v3
42 | },
43 | timestamp: {Scrubbed},
44 | sendToExperiment: true
45 | }
46 | ]
47 | }
48 | ]
--------------------------------------------------------------------------------
/tests/FeatBit.ServerSdk.Tests/Events/DefaultEventSerializerTests.SerializeMetricEvent.verified.txt:
--------------------------------------------------------------------------------
1 | {
2 | user: {
3 | keyId: u1-Id,
4 | name: u1-name,
5 | customizedProperties: [
6 | {
7 | name: custom,
8 | value: value
9 | },
10 | {
11 | name: country,
12 | value: us
13 | }
14 | ]
15 | },
16 | metrics: [
17 | {
18 | appType: dotnet-server-side,
19 | route: index/metric,
20 | type: CustomEvent,
21 | eventName: click-button,
22 | numericValue: 1.5,
23 | timestamp: {Scrubbed}
24 | }
25 | ]
26 | }
--------------------------------------------------------------------------------
/tests/FeatBit.ServerSdk.Tests/Events/DefaultEventSerializerTests.SerializeMetricEvents.verified.txt:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | user: {
4 | keyId: u1-Id,
5 | name: u1-name,
6 | customizedProperties: [
7 | {
8 | name: custom,
9 | value: value
10 | },
11 | {
12 | name: country,
13 | value: us
14 | }
15 | ]
16 | },
17 | metrics: [
18 | {
19 | appType: dotnet-server-side,
20 | route: index/metric,
21 | type: CustomEvent,
22 | eventName: click-button,
23 | numericValue: 1.5,
24 | timestamp: {Scrubbed}
25 | }
26 | ]
27 | },
28 | {
29 | user: {
30 | keyId: u2-Id,
31 | name: u2-name,
32 | customizedProperties: [
33 | {
34 | name: age,
35 | value: 10
36 | }
37 | ]
38 | },
39 | metrics: [
40 | {
41 | appType: dotnet-server-side,
42 | route: index/metric,
43 | type: CustomEvent,
44 | eventName: click-button,
45 | numericValue: 32.5,
46 | timestamp: {Scrubbed}
47 | }
48 | ]
49 | }
50 | ]
--------------------------------------------------------------------------------
/tests/FeatBit.ServerSdk.Tests/Events/DefaultEventSerializerTests.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 | using FeatBit.Sdk.Server.Model;
3 |
4 | namespace FeatBit.Sdk.Server.Events;
5 |
6 | [UsesVerify]
7 | public class DefaultEventSerializerTests
8 | {
9 | [Fact]
10 | public async Task SerializeEvalEvent()
11 | {
12 | var serializer = new DefaultEventSerializer();
13 |
14 | var @event = AllEvalEvents()[0];
15 |
16 | var jsonBytes = serializer.Serialize(@event);
17 | var json = Encoding.UTF8.GetString(jsonBytes);
18 |
19 | await VerifyJson(json).ScrubMember("timestamp");
20 | }
21 |
22 | [Fact]
23 | public async Task SerializeMetricEvent()
24 | {
25 | var serializer = new DefaultEventSerializer();
26 |
27 | var @event = AllMetricEvents()[0];
28 |
29 | var jsonBytes = serializer.Serialize(@event);
30 | var json = Encoding.UTF8.GetString(jsonBytes);
31 |
32 | await VerifyJson(json).ScrubMember("timestamp");
33 | }
34 |
35 | [Fact]
36 | public async Task SerializeEvalEvents()
37 | {
38 | var serializer = new DefaultEventSerializer();
39 |
40 | var events = AllEvalEvents();
41 | var result = new ReadOnlyMemory(events, 1, 2);
42 |
43 | var jsonBytes = serializer.Serialize(result);
44 | var json = Encoding.UTF8.GetString(jsonBytes);
45 |
46 | await VerifyJson(json).ScrubMember("timestamp");
47 | }
48 |
49 | [Fact]
50 | public async Task SerializeMetricEvents()
51 | {
52 | var serializer = new DefaultEventSerializer();
53 |
54 | var events = AllMetricEvents();
55 | var result = new ReadOnlyMemory(events, 0, 2);
56 |
57 | var jsonBytes = serializer.Serialize(result);
58 | var json = Encoding.UTF8.GetString(jsonBytes);
59 |
60 | await VerifyJson(json).ScrubMember("timestamp");
61 | }
62 |
63 | [Fact]
64 | public async Task SerializeCombinedEvents()
65 | {
66 | var serializer = new DefaultEventSerializer();
67 |
68 | var events = new[] { AllEvalEvents()[0], AllMetricEvents()[0] };
69 | var result = new ReadOnlyMemory(events, 0, 2);
70 |
71 | var jsonBytes = serializer.Serialize(result);
72 | var json = Encoding.UTF8.GetString(jsonBytes);
73 |
74 | await VerifyJson(json).ScrubMember("timestamp");
75 | }
76 |
77 | private static IEvent[] AllEvalEvents()
78 | {
79 | var user1 = FbUser.Builder("u1-Id")
80 | .Name("u1-name")
81 | .Custom("custom", "value")
82 | .Custom("country", "us")
83 | .Build();
84 | var v1Variation = new Variation
85 | {
86 | Id = "v1Id",
87 | Value = "v1"
88 | };
89 | var event1 = new EvalEvent(user1, "hello", v1Variation, true);
90 |
91 | var user2 = FbUser.Builder("u2-Id")
92 | .Name("u2-name")
93 | .Custom("age", "10")
94 | .Build();
95 | var v2Variation = new Variation
96 | {
97 | Id = "v2Id",
98 | Value = "v2"
99 | };
100 | var event2 = new EvalEvent(user2, "hello", v2Variation, false);
101 |
102 | var user3 = FbUser.Builder("u3-Id")
103 | .Name("u3-name")
104 | .Custom("age", "10")
105 | .Build();
106 | var v3Variation = new Variation
107 | {
108 | Id = "v3Id",
109 | Value = "v3"
110 | };
111 | var event3 = new EvalEvent(user3, "hello", v3Variation, true);
112 |
113 | return new IEvent[] { event1, event2, event3 };
114 | }
115 |
116 | private static IEvent[] AllMetricEvents()
117 | {
118 | var user1 = FbUser.Builder("u1-Id")
119 | .Name("u1-name")
120 | .Custom("custom", "value")
121 | .Custom("country", "us")
122 | .Build();
123 | var event1 = new MetricEvent(user1, "click-button", 1.5d);
124 |
125 | var user2 = FbUser.Builder("u2-Id")
126 | .Name("u2-name")
127 | .Custom("age", "10")
128 | .Build();
129 | var event2 = new MetricEvent(user2, "click-button", 32.5d);
130 |
131 | var user3 = FbUser.Builder("u3-Id")
132 | .Name("u3-name")
133 | .Custom("age", "10")
134 | .Build();
135 | var event3 = new MetricEvent(user3, "click-button", 26.5d);
136 |
137 | return new IEvent[] { event1, event2, event3 };
138 | }
139 | }
--------------------------------------------------------------------------------
/tests/FeatBit.ServerSdk.Tests/Events/IntEvent.cs:
--------------------------------------------------------------------------------
1 | namespace FeatBit.Sdk.Server.Events;
2 |
3 | internal sealed class IntEvent : PayloadEvent
4 | {
5 | public int Value { get; }
6 |
7 | public IntEvent(int value)
8 | {
9 | Value = value;
10 | }
11 | }
--------------------------------------------------------------------------------
/tests/FeatBit.ServerSdk.Tests/FbClientOfflineTests.cs:
--------------------------------------------------------------------------------
1 | using FeatBit.Sdk.Server.DataSynchronizer;
2 | using FeatBit.Sdk.Server.Evaluation;
3 | using FeatBit.Sdk.Server.Events;
4 | using FeatBit.Sdk.Server.Model;
5 | using FeatBit.Sdk.Server.Options;
6 |
7 | namespace FeatBit.Sdk.Server;
8 |
9 | public class FbClientOfflineTests
10 | {
11 | [Fact]
12 | public async Task CreateAndClose()
13 | {
14 | var options = new FbOptionsBuilder()
15 | .Offline(true)
16 | .Build();
17 |
18 | var client = new FbClient(options);
19 | await client.CloseAsync();
20 | }
21 |
22 | [Fact]
23 | public void UseNullDataSource()
24 | {
25 | var options = new FbOptionsBuilder()
26 | .Offline(true)
27 | .Build();
28 |
29 | var client = new FbClient(options);
30 |
31 | Assert.IsType(client._dataSynchronizer);
32 | }
33 |
34 | [Fact]
35 | public void UseNullEventProcessor()
36 | {
37 | var options = new FbOptionsBuilder()
38 | .Offline(true)
39 | .Build();
40 |
41 | var client = new FbClient(options);
42 |
43 | Assert.IsType(client._eventProcessor);
44 | }
45 |
46 | [Fact]
47 | public void UseNullEventProcessorWhenEventsAreDisabled()
48 | {
49 | var options = new FbOptionsBuilder()
50 | .Offline(false)
51 | .DisableEvents(true)
52 | .Build();
53 |
54 | var client = new FbClient(options);
55 |
56 | Assert.IsType(client._eventProcessor);
57 | }
58 |
59 | [Fact]
60 | public void ClientIsInitialized()
61 | {
62 | var options = new FbOptionsBuilder()
63 | .Offline(true)
64 | .Build();
65 |
66 | var client = new FbClient(options);
67 |
68 | Assert.True(client.Initialized);
69 | }
70 |
71 | [Fact]
72 | public void EvaluationReturnsDefaultValue()
73 | {
74 | var options = new FbOptionsBuilder()
75 | .Offline(true)
76 | .Build();
77 |
78 | var client = new FbClient(options);
79 |
80 | var user = FbUser.Builder("tester").Build();
81 |
82 | var variationDetail = client.StringVariationDetail("hello", user, "fallback-value");
83 | Assert.Equal("fallback-value", variationDetail.Value);
84 | Assert.Equal(ReasonKind.Error, variationDetail.Kind);
85 | Assert.Equal("flag not found", variationDetail.Reason);
86 | }
87 |
88 | [Fact]
89 | public void WithJsonBootstrapProvider()
90 | {
91 | var options = new FbOptionsBuilder()
92 | .Offline(true)
93 | .UseJsonBootstrapProvider(TestData.BootstrapJson)
94 | .Build();
95 |
96 | var client = new FbClient(options);
97 |
98 | var user = FbUser.Builder("true-1").Build();
99 | var variationDetail = client.BoolVariationDetail("example-flag", user);
100 |
101 | Assert.True(variationDetail.Value);
102 | Assert.Equal("target match", variationDetail.Reason);
103 | Assert.Equal(ReasonKind.TargetMatch, variationDetail.Kind);
104 | }
105 | }
--------------------------------------------------------------------------------
/tests/FeatBit.ServerSdk.Tests/FbClientTests.cs:
--------------------------------------------------------------------------------
1 | using FeatBit.Sdk.Server.DataSynchronizer;
2 | using FeatBit.Sdk.Server.Evaluation;
3 | using FeatBit.Sdk.Server.Events;
4 | using FeatBit.Sdk.Server.Model;
5 | using FeatBit.Sdk.Server.Options;
6 | using FeatBit.Sdk.Server.Store;
7 |
8 | namespace FeatBit.Sdk.Server;
9 |
10 | [Collection(nameof(TestApp))]
11 | public class FbClientTests
12 | {
13 | private readonly TestApp _app;
14 |
15 | public FbClientTests(TestApp app)
16 | {
17 | _app = app;
18 | }
19 |
20 | [Fact]
21 | public async Task CloseInitializedFbClient()
22 | {
23 | var client = CreateTestFbClient();
24 | Assert.True(client.Initialized);
25 |
26 | await client.CloseAsync();
27 | }
28 |
29 | [Fact]
30 | public async Task CloseUninitializedFbClient()
31 | {
32 | var options = new FbOptionsBuilder("fake-secret")
33 | .ConnectTimeout(TimeSpan.FromMilliseconds(50))
34 | .StartWaitTime(TimeSpan.FromMilliseconds(100))
35 | .Build();
36 | var client = new FbClient(options);
37 | Assert.False(client.Initialized);
38 |
39 | await client.CloseAsync();
40 | }
41 |
42 | [Fact]
43 | public void GetVariation()
44 | {
45 | var eventProcessorMock = new Mock();
46 | var client = CreateTestFbClient(eventProcessorMock.Object);
47 |
48 | var user = FbUser.Builder("u1").Build();
49 | var variation = client.BoolVariation("returns-true", user);
50 | Assert.True(variation);
51 |
52 | eventProcessorMock.Verify(x => x.Record(It.IsAny()), Times.Once);
53 | }
54 |
55 | [Fact]
56 | public void GetVariationDetail()
57 | {
58 | var eventProcessorMock = new Mock();
59 | var client = CreateTestFbClient(eventProcessorMock.Object);
60 |
61 | var user = FbUser.Builder("u1").Build();
62 | var variationDetail = client.BoolVariationDetail("returns-true", user);
63 | Assert.Equal("returns-true", variationDetail.Key);
64 | Assert.True(variationDetail.Value);
65 | Assert.Equal("3da96792-debf-4878-905a-c9b5f9178cd0", variationDetail.ValueId);
66 | Assert.Equal(ReasonKind.Fallthrough, variationDetail.Kind);
67 | Assert.Equal("fall through targets and rules", variationDetail.Reason);
68 |
69 | eventProcessorMock.Verify(x => x.Record(It.IsAny()), Times.Once);
70 | }
71 |
72 | [Fact]
73 | public void GetAllVariations()
74 | {
75 | var client = CreateTestFbClient();
76 | var user = FbUser.Builder("u1").Build();
77 |
78 | var results = client.GetAllVariations(user);
79 | Assert.Single(results);
80 |
81 | var result0 = results[0];
82 | Assert.Equal("returns-true", result0.Key);
83 | Assert.Equal("true", result0.Value);
84 | Assert.Equal("3da96792-debf-4878-905a-c9b5f9178cd0", result0.ValueId);
85 | Assert.Equal(ReasonKind.Fallthrough, result0.Kind);
86 | Assert.Equal("fall through targets and rules", result0.Reason);
87 | }
88 |
89 | private FbClient CreateTestFbClient(IEventProcessor processor = null)
90 | {
91 | var options = new FbOptionsBuilder("qJHQTVfsZUOu1Q54RLMuIQ-JtrIvNK-k-bARYicOTNQA")
92 | .Streaming(new Uri("ws://localhost/"))
93 | .Build();
94 |
95 | var store = new DefaultMemoryStore();
96 | var synchronizer =
97 | new WebSocketDataSynchronizer(options, store, op => _app.CreateFbWebSocket(op));
98 | var eventProcessor = processor ?? new DefaultEventProcessor(options);
99 | var client = new FbClient(options, store, synchronizer, eventProcessor);
100 | return client;
101 | }
102 | }
--------------------------------------------------------------------------------
/tests/FeatBit.ServerSdk.Tests/FeatBit.ServerSdk.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | FeatBit.ServerSdk.Tests
6 | FeatBit.ServerSdk.Tests
7 | false
8 | true
9 | FeatBit.Sdk.Server
10 | true
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/tests/FeatBit.ServerSdk.Tests/Model/DeserializationTests.DeserializeFeatureFlag.verified.txt:
--------------------------------------------------------------------------------
1 | {
2 | StoreKey: ff_example,
3 | Id: Guid_1,
4 | Key: example,
5 | VariationType: boolean,
6 | Variations: [
7 | {
8 | Id: Guid_2,
9 | Value: true
10 | },
11 | {
12 | Id: Guid_3,
13 | Value: false
14 | }
15 | ],
16 | TargetUsers: [
17 | {
18 | KeyIds: [
19 | true-1,
20 | true-2,
21 | true-3,
22 | true-4,
23 | true-5,
24 | true-6,
25 | true-7,
26 | true-8,
27 | true-9,
28 | true-10
29 | ],
30 | VariationId: Guid_2
31 | },
32 | {
33 | KeyIds: [
34 | false-1,
35 | false-2,
36 | false-3,
37 | false-4,
38 | false-5
39 | ],
40 | VariationId: Guid_3
41 | }
42 | ],
43 | Rules: [
44 | {
45 | Name: Rule 1,
46 | IncludedInExpt: false,
47 | Conditions: [
48 | {
49 | Property: age,
50 | Op: BiggerThan,
51 | Value: 22
52 | },
53 | {
54 | Property: name,
55 | Op: EndsWith,
56 | Value: _qa
57 | }
58 | ],
59 | Variations: [
60 | {
61 | Id: Guid_2,
62 | Rollout: [
63 | 0.0,
64 | 1.0
65 | ],
66 | ExptRollout: 1.0
67 | }
68 | ]
69 | },
70 | {
71 | Name: Rule 2,
72 | DispatchKey: keyId,
73 | IncludedInExpt: false,
74 | Conditions: [
75 | {
76 | Property: country,
77 | Op: IsOneOf,
78 | Value: ["cn","us","jp","gb","es","ss"]
79 | }
80 | ],
81 | Variations: [
82 | {
83 | Id: Guid_2,
84 | Rollout: [
85 | 0.0,
86 | 0.2
87 | ],
88 | ExptRollout: 1.0
89 | },
90 | {
91 | Id: Guid_3,
92 | Rollout: [
93 | 0.2,
94 | 1.0
95 | ],
96 | ExptRollout: 1.0
97 | }
98 | ]
99 | }
100 | ],
101 | IsEnabled: true,
102 | DisabledVariationId: Guid_3,
103 | Fallthrough: {
104 | IncludedInExpt: true,
105 | Variations: [
106 | {
107 | Id: Guid_2,
108 | Rollout: [
109 | 0.0,
110 | 1.0
111 | ],
112 | ExptRollout: 1.0
113 | }
114 | ]
115 | },
116 | ExptIncludeAllTargets: true,
117 | Version: 1674871495616
118 | }
--------------------------------------------------------------------------------
/tests/FeatBit.ServerSdk.Tests/Model/DeserializationTests.DeserializeSegment.verified.txt:
--------------------------------------------------------------------------------
1 | {
2 | StoreKey: segment_0779d76b-afc6-4886-ab65-af8c004273ad,
3 | Id: Guid_1,
4 | Included: [
5 | true-6,
6 | true-5,
7 | true-4,
8 | true-3,
9 | true-1,
10 | true-2,
11 | true-7,
12 | true-8,
13 | true-9,
14 | true-10
15 | ],
16 | Excluded: [
17 | false-1,
18 | false-2,
19 | false-3,
20 | false-4,
21 | false-5
22 | ],
23 | Rules: [
24 | {
25 | Conditions: [
26 | {
27 | Property: age,
28 | Op: LessEqualThan,
29 | Value: 22
30 | },
31 | {
32 | Property: country,
33 | Op: IsOneOf,
34 | Value: ["cn","us","es"]
35 | },
36 | {
37 | Property: name,
38 | Op: NotEqual,
39 | Value: bob
40 | },
41 | {
42 | Property: isMember,
43 | Op: IsTrue,
44 | Value: IsTrue
45 | }
46 | ]
47 | }
48 | ],
49 | Version: 1674885283583
50 | }
--------------------------------------------------------------------------------
/tests/FeatBit.ServerSdk.Tests/Model/DeserializationTests.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json;
2 | using FeatBit.Sdk.Server.Json;
3 |
4 | namespace FeatBit.Sdk.Server.Model;
5 |
6 | [UsesVerify]
7 | public class DeserializationTests
8 | {
9 | [Fact]
10 | public Task DeserializeFeatureFlag()
11 | {
12 | var json = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "Model", "one-flag.json"));
13 | var flag = JsonSerializer.Deserialize(json, ReusableJsonSerializerOptions.Web);
14 |
15 | return Verify(flag);
16 | }
17 |
18 | [Fact]
19 | public Task DeserializeSegment()
20 | {
21 | var json = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "Model", "one-segment.json"));
22 | var segment = JsonSerializer.Deserialize(json, ReusableJsonSerializerOptions.Web);
23 |
24 | return Verify(segment);
25 | }
26 | }
--------------------------------------------------------------------------------
/tests/FeatBit.ServerSdk.Tests/Model/one-flag.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "174c7138-426d-4434-8a91-af8c00306de3",
3 | "createdAt": "2023-01-16T02:56:19.531Z",
4 | "updatedAt": "2023-01-28T02:04:55.616Z",
5 | "creatorId": "197dea2a-1044-497d-8f1e-f01d2e15a756",
6 | "updatorId": "197dea2a-1044-497d-8f1e-f01d2e15a756",
7 | "envId": "226b9bf8-4af3-4ffa-9b01-162270e4cd40",
8 | "name": "example",
9 | "key": "example",
10 | "variationType": "boolean",
11 | "variations": [
12 | {
13 | "id": "3da96792-debf-4878-905a-c9b5f9178cd0",
14 | "value": "true"
15 | },
16 | {
17 | "id": "19cf9fb6-b790-4877-ab96-b5c5e73e0b46",
18 | "value": "false"
19 | }
20 | ],
21 | "targetUsers": [
22 | {
23 | "keyIds": [
24 | "true-1",
25 | "true-2",
26 | "true-3",
27 | "true-4",
28 | "true-5",
29 | "true-6",
30 | "true-7",
31 | "true-8",
32 | "true-9",
33 | "true-10"
34 | ],
35 | "variationId": "3da96792-debf-4878-905a-c9b5f9178cd0"
36 | },
37 | {
38 | "keyIds": [
39 | "false-1",
40 | "false-2",
41 | "false-3",
42 | "false-4",
43 | "false-5"
44 | ],
45 | "variationId": "19cf9fb6-b790-4877-ab96-b5c5e73e0b46"
46 | }
47 | ],
48 | "rules": [
49 | {
50 | "id": "16273e69-8e1f-4828-bb9b-07576724f6d8",
51 | "name": "Rule 1",
52 | "dispatchKey": null,
53 | "includedInExpt": false,
54 | "conditions": [
55 | {
56 | "property": "age",
57 | "op": "BiggerThan",
58 | "value": "22"
59 | },
60 | {
61 | "property": "name",
62 | "op": "EndsWith",
63 | "value": "_qa"
64 | }
65 | ],
66 | "variations": [
67 | {
68 | "id": "3da96792-debf-4878-905a-c9b5f9178cd0",
69 | "rollout": [
70 | 0,
71 | 1
72 | ],
73 | "exptRollout": 1
74 | }
75 | ]
76 | },
77 | {
78 | "id": "05a6e5fa-6fb5-4bf8-8c2d-d556ed44c143",
79 | "name": "Rule 2",
80 | "dispatchKey": "keyId",
81 | "includedInExpt": false,
82 | "conditions": [
83 | {
84 | "property": "country",
85 | "op": "IsOneOf",
86 | "value": "[\"cn\",\"us\",\"jp\",\"gb\",\"es\",\"ss\"]"
87 | }
88 | ],
89 | "variations": [
90 | {
91 | "id": "3da96792-debf-4878-905a-c9b5f9178cd0",
92 | "rollout": [
93 | 0,
94 | 0.2
95 | ],
96 | "exptRollout": 1
97 | },
98 | {
99 | "id": "19cf9fb6-b790-4877-ab96-b5c5e73e0b46",
100 | "rollout": [
101 | 0.2,
102 | 1
103 | ],
104 | "exptRollout": 1
105 | }
106 | ]
107 | }
108 | ],
109 | "isEnabled": true,
110 | "disabledVariationId": "19cf9fb6-b790-4877-ab96-b5c5e73e0b46",
111 | "fallthrough": {
112 | "dispatchKey": null,
113 | "includedInExpt": true,
114 | "variations": [
115 | {
116 | "id": "3da96792-debf-4878-905a-c9b5f9178cd0",
117 | "rollout": [
118 | 0,
119 | 1
120 | ],
121 | "exptRollout": 1
122 | }
123 | ]
124 | },
125 | "exptIncludeAllTargets": true,
126 | "tags": [],
127 | "isArchived": false
128 | }
--------------------------------------------------------------------------------
/tests/FeatBit.ServerSdk.Tests/Model/one-segment.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "0779d76b-afc6-4886-ab65-af8c004273ad",
3 | "createdAt": "2023-01-16T04:01:56.63Z",
4 | "updatedAt": "2023-01-28T05:54:43.583Z",
5 | "envId": "226b9bf8-4af3-4ffa-9b01-162270e4cd40",
6 | "name": "segment-01",
7 | "description": "- 10 users in individual targeting return true, 5 returns false\n- Cover types of attribute: Number, String, List, Boolean",
8 | "included": [
9 | "true-6",
10 | "true-5",
11 | "true-4",
12 | "true-3",
13 | "true-1",
14 | "true-2",
15 | "true-7",
16 | "true-8",
17 | "true-9",
18 | "true-10"
19 | ],
20 | "excluded": [
21 | "false-1",
22 | "false-2",
23 | "false-3",
24 | "false-4",
25 | "false-5"
26 | ],
27 | "rules": [
28 | {
29 | "id": "19a77402-3ee7-4ea0-83bc-3e019b982d1b",
30 | "name": "Rule 1",
31 | "conditions": [
32 | {
33 | "property": "age",
34 | "op": "LessEqualThan",
35 | "value": "22"
36 | },
37 | {
38 | "property": "country",
39 | "op": "IsOneOf",
40 | "value": "[\"cn\",\"us\",\"es\"]"
41 | },
42 | {
43 | "property": "name",
44 | "op": "NotEqual",
45 | "value": "bob"
46 | },
47 | {
48 | "property": "isMember",
49 | "op": "IsTrue",
50 | "value": "IsTrue"
51 | }
52 | ]
53 | }
54 | ],
55 | "isArchived": false
56 | }
--------------------------------------------------------------------------------
/tests/FeatBit.ServerSdk.Tests/Options/FbOptionsBuilderTests.HasDefaultValues.verified.txt:
--------------------------------------------------------------------------------
1 | {
2 | StartWaitTime: 00:00:05,
3 | Offline: false,
4 | DisableEvents: false,
5 | EnvSecret: secret,
6 | StreamingUri: ws://localhost:5100,
7 | EventUri: http://localhost:5100,
8 | ConnectTimeout: 00:00:03,
9 | CloseTimeout: 00:00:02,
10 | KeepAliveInterval: 00:00:15,
11 | ReconnectRetryDelays: [
12 | 00:00:00,
13 | 00:00:01,
14 | 00:00:02,
15 | 00:00:03,
16 | 00:00:05,
17 | 00:00:08,
18 | 00:00:13,
19 | 00:00:21,
20 | 00:00:34,
21 | 00:00:55
22 | ],
23 | FlushTimeout: 00:00:05,
24 | MaxFlushWorker: {Scrubbed},
25 | AutoFlushInterval: 00:00:05,
26 | MaxEventsInQueue: 10000,
27 | MaxEventPerRequest: 50,
28 | MaxSendEventAttempts: 2,
29 | SendEventRetryInterval: 00:00:00.2000000
30 | }
--------------------------------------------------------------------------------
/tests/FeatBit.ServerSdk.Tests/Options/FbOptionsBuilderTests.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json;
2 | using FeatBit.Sdk.Server.Bootstrapping;
3 | using Microsoft.Extensions.Logging.Abstractions;
4 |
5 | namespace FeatBit.Sdk.Server.Options;
6 |
7 | [UsesVerify]
8 | public class FbOptionsBuilderTests
9 | {
10 | [Fact]
11 | public async Task HasDefaultValues()
12 | {
13 | var builder = new FbOptionsBuilder("secret");
14 | var options = builder.Build();
15 |
16 | Assert.IsType(options.LoggerFactory);
17 | Assert.IsType(options.BootstrapProvider);
18 |
19 | await Verify(options)
20 | // max flush worker depends on the number of processors available on the machine
21 | .ScrubMember(x => x.MaxFlushWorker)
22 | .IgnoreMembers(x => x.LoggerFactory, x => x.BootstrapProvider);
23 | }
24 |
25 | [Fact]
26 | public void NoSecretProvided()
27 | {
28 | var options = new FbOptionsBuilder().Build();
29 |
30 | Assert.Equal(string.Empty, options.EnvSecret);
31 | }
32 |
33 | [Fact]
34 | public void SetBootstrapProvider()
35 | {
36 | var data = new
37 | {
38 | messageType = "data-sync",
39 | data = new
40 | {
41 | eventType = "full",
42 | featureFlags = Array.Empty