public void FlagReturnsFallthroughVariationAndEventIfPrerequisiteIsMetAndThereAreNoRules() { var f0 = new FeatureFlagBuilder("feature0") .On(true) .Prerequisites(new Prerequisite("feature1", 1)) .OffVariation(1) .FallthroughVariation(0) .Variations(fallthroughValue, offValue, onValue) .Version(1) .Build(); var f1 = new FeatureFlagBuilder("feature1") .On(true) .FallthroughVariation(1) // this is what makes the prerequisite pass .Variations(LdValue.Of("nogo"), LdValue.Of("go")) .Version(2) .Build(); var evaluator = BasicEvaluator.WithStoredFlags(f1); var result = evaluator.Evaluate(f0, baseUser, EventFactory.Default); var expected = new EvaluationDetail <LdValue>(fallthroughValue, 0, EvaluationReason.FallthroughReason); Assert.Equal(expected, result.Result); Assert.Equal(1, result.PrerequisiteEvents.Count); EvaluationEvent e = result.PrerequisiteEvents[0]; Assert.Equal(f1.Key, e.FlagKey); Assert.Equal(LdValue.Of("go"), e.Value); Assert.Equal(f1.Version, e.FlagVersion); Assert.Equal(f0.Key, e.PrerequisiteOf); }
public void SerializeFlagWithAllProperties() { var flag1 = new FeatureFlagBuilder() .Version(1) .Value(LdValue.Of(false)) .Variation(2) .FlagVersion(3) .Reason(EvaluationReason.OffReason) .TrackEvents(true) .DebugEventsUntilDate(UnixMillisecondTime.OfMillis(1234)) .Build(); AssertJsonEqual(@"{""version"":1,""value"":false,""variation"":2,""flagVersion"":3," + @"""reason"":{""kind"":""OFF""},""trackEvents"":true,""debugEventsUntilDate"":1234}", DataModelSerialization.SerializeFlag(flag1)); // make sure we're treating trackReason separately from trackEvents var flag2 = new FeatureFlagBuilder() .Version(1) .Value(LdValue.Of(false)) .Reason(EvaluationReason.OffReason) .Variation(2) .FlagVersion(3) .TrackReason(true) .Build(); AssertJsonEqual(@"{""version"":1,""value"":false,""variation"":2,""flagVersion"":3," + @"""reason"":{""kind"":""OFF""},""trackReason"":true}", DataModelSerialization.SerializeFlag(flag2)); }
public void AllFlagsStateCanFilterForOnlyClientSideFlags() { var flag1 = new FeatureFlagBuilder("server-side-1").Build(); var flag2 = new FeatureFlagBuilder("server-side-2").Build(); var flag3 = new FeatureFlagBuilder("client-side-1").ClientSide(true) .OffWithValue(LdValue.Of("value1")).Build(); var flag4 = new FeatureFlagBuilder("client-side-2").ClientSide(true) .OffWithValue(LdValue.Of("value2")).Build(); testData.UsePreconfiguredFlag(flag1); testData.UsePreconfiguredFlag(flag2); testData.UsePreconfiguredFlag(flag3); testData.UsePreconfiguredFlag(flag4); var state = client.AllFlagsState(user, FlagsStateOption.ClientSideOnly); Assert.True(state.Valid); var expectedValues = new Dictionary <string, LdValue> { { "client-side-1", LdValue.Of("value1") }, { "client-side-2", LdValue.Of("value2") } }; Assert.Equal(expectedValues, state.ToValuesJsonMap()); }
public void AddToData_DuplicateKeysHandling_Throw() { const string key = "flag1"; FeatureFlag initialFeatureFlag = new FeatureFlagBuilder(key).Version(0).Build(); var flagData = new Dictionary <string, ItemDescriptor> { { key, new ItemDescriptor(1, initialFeatureFlag) } }; var segmentData = new Dictionary <string, ItemDescriptor>(); var fileData = new DataSetBuilder() .Flags(new FeatureFlagBuilder(key).Version(1).Build()).Build(); FlagFileDataMerger merger = new FlagFileDataMerger(FileDataTypes.DuplicateKeysHandling.Throw); Exception err = Assert.Throws <Exception>(() => { merger.AddToData(fileData, flagData, segmentData); }); Assert.Equal("in \"features\", key \"flag1\" was already defined", err.Message); ItemDescriptor postFeatureFlag = flagData[key]; Assert.Same(initialFeatureFlag, postFeatureFlag.Item); Assert.Equal(1, postFeatureFlag.Version); }
public void AddToData_DuplicateKeysHandling_Ignore() { const string key = "flag1"; FeatureFlag initialFeatureFlag = new FeatureFlagBuilder(key).Version(0).Build(); var flagData = new Dictionary <string, ItemDescriptor> { { key, new ItemDescriptor(1, initialFeatureFlag) } }; var segmentData = new Dictionary <string, ItemDescriptor>(); FlagFileData fileData = new FlagFileData { Flags = new Dictionary <string, FeatureFlag> { { key, new FeatureFlagBuilder(key).Version(1).Build() } } }; FlagFileDataMerger merger = new FlagFileDataMerger(FileDataTypes.DuplicateKeysHandling.Ignore); merger.AddToData(fileData, flagData, segmentData); ItemDescriptor postFeatureFlag = flagData[key]; Assert.Same(initialFeatureFlag, postFeatureFlag.Item); Assert.Equal(1, postFeatureFlag.Version); }
public void FlagReturnsOffVariationAndEventIfPrerequisiteIsOff() { var f0 = new FeatureFlagBuilder("feature0") .On(true) .Prerequisites(new Prerequisite("feature1", 1)) .OffVariation(1) .FallthroughVariation(0) .Variations(fallthroughValue, offValue, onValue) .Version(1) .Build(); var f1 = new FeatureFlagBuilder("feature1") .On(false) .OffVariation(1) // note that even though it returns the desired variation, it is still off and therefore not a match .Variations(LdValue.Of("nogo"), LdValue.Of("go")) .Version(2) .Build(); var evaluator = BasicEvaluator.WithStoredFlags(f1); var result = evaluator.Evaluate(f0, baseUser, EventFactory.Default); var expected = new EvaluationDetail <LdValue>(offValue, 1, EvaluationReason.PrerequisiteFailedReason("feature1")); Assert.Equal(expected, result.Result); Assert.Equal(1, result.PrerequisiteEvents.Count); EvaluationEvent e = result.PrerequisiteEvents[0]; Assert.Equal(f1.Key, e.FlagKey); Assert.Equal(LdValue.Of("go"), e.Value); Assert.Equal(f1.Version, e.FlagVersion); Assert.Equal(f0.Key, e.PrerequisiteOf); }
public void UpsertUpdatesPersistentStore() { var flag1a = new FeatureFlagBuilder().Version(1).Value(true).Build(); var flag1b = new FeatureFlagBuilder().Version(2).Value(true).Build(); var flag2 = new FeatureFlagBuilder().Version(1).Value(false).Build(); var data1a = new DataSetBuilder().Add("flag1", flag1a).Add("flag2", flag2).Build(); var data1b = new DataSetBuilder().Add("flag1", flag1b).Add("flag2", flag2).Build(); var store = MakeStore(1); store.Init(BasicUser, data1a, true); var updated = store.Upsert("flag1", flag1b.ToItemDescriptor()); Assert.True(updated); var item = store.Get("flag1"); // this is reading only from memory, not the persistent store Assert.Equal(flag1b.ToItemDescriptor(), item); var data = _persistentStore.InspectUserData(BasicMobileKey, BasicUser.Key); Assert.NotNull(data); AssertHelpers.DataSetsEqual(data1b, data.Value); }
public void EventTrackingAndReasonAreNotForcedIfFlagIsNotSetForMatchingRule() { var clause0 = ClauseBuilder.ShouldNotMatchUser(user); var clause1 = ClauseBuilder.ShouldMatchUser(user); var rule0 = new RuleBuilder().Id("id0").Variation(1).Clauses(clause0).TrackEvents(true).Build(); var rule1 = new RuleBuilder().Id("id1").Variation(1).Clauses(clause1).TrackEvents(false).Build(); var flag = new FeatureFlagBuilder("flag").Version(1) .On(true) .Rules(rule0, rule1) .OffVariation(0) .Variations(LdValue.Of("off"), LdValue.Of("on")) .Build(); testData.UsePreconfiguredFlag(flag); client.StringVariation("flag", user, "default"); // It matched rule1, which has trackEvents: false, so we don't get the override behavior Assert.Equal(1, eventSink.Events.Count); var e = Assert.IsType <EvaluationEvent>(eventSink.Events[0]); Assert.False(e.TrackEvents); Assert.Null(e.Reason); }
public void EventIsSentForExistingPrerequisiteFlag() { var f0 = new FeatureFlagBuilder("feature0").Version(1) .On(true) .Prerequisites(new Prerequisite("feature1", 1)) .Fallthrough(new VariationOrRollout(0, null)) .OffVariation(1) .Variations(LdValue.Of("fall"), LdValue.Of("off"), LdValue.Of("on")) .Version(1) .Build(); var f1 = new FeatureFlagBuilder("feature1").Version(1) .On(true) .Fallthrough(new VariationOrRollout(1, null)) .Variations(LdValue.Of("nogo"), LdValue.Of("go")) .Version(2) .Build(); testData.UsePreconfiguredFlag(f0); testData.UsePreconfiguredFlag(f1); client.StringVariation("feature0", user, "default"); Assert.Equal(2, eventSink.Events.Count); CheckFeatureEvent(eventSink.Events[0], f1, LdValue.Of("go"), LdValue.Null, "feature0"); CheckFeatureEvent(eventSink.Events[1], f0, LdValue.Of("fall"), LdValue.Of("default"), null); }
public async Task WhenUserNotInFlags_DoesntSave() { // Arrange var flags = new FeatureFlagBuilder() .WithFlight("A", accounts: new List <string> { "user1", "user2" }) .Build(); _storage .Setup(s => s.GetFileReferenceAsync(CoreConstants.Folders.ContentFolderName, CoreConstants.FeatureFlagsFileName, null)) .ReturnsAsync(BuildFileReference(flags)); // Act await _target.RemoveUserAsync(new User { Username = "******" }); // Assert _storage.Verify( s => s.SaveFileAsync( It.IsAny <string>(), It.IsAny <string>(), It.IsAny <Stream>(), It.IsAny <IAccessCondition>()), Times.Never); _auditing.Verify( a => a.SaveAuditRecordAsync( It.IsAny <FeatureFlagsAuditRecord>()), Times.Never()); }
public void AllFlagsStateReturnsStateWithReasons() { var flag1 = new FeatureFlagBuilder("key1").Version(100) .OffVariation(0).Variations(LdValue.Of("value1")) .Build(); var flag2 = new FeatureFlagBuilder("key2").Version(200) .OffVariation(1).Variations(LdValue.Of("x"), LdValue.Of("value2")) .TrackEvents(true).DebugEventsUntilDate(UnixMillisecondTime.OfMillis(1000)) .Build(); testData.UsePreconfiguredFlag(flag1); testData.UsePreconfiguredFlag(flag2); var state = client.AllFlagsState(user, FlagsStateOption.WithReasons); Assert.True(state.Valid); var expectedString = @"{""key1"":""value1"",""key2"":""value2"", ""$flagsState"":{ ""key1"":{ ""variation"":0,""version"":100,""reason"":{""kind"":""OFF""} },""key2"":{ ""variation"":1,""version"":200,""reason"":{""kind"":""OFF""},""trackEvents"":true,""debugEventsUntilDate"":1000 } }, ""$valid"":true }"; var actualString = LdJsonSerialization.SerializeObject(state); AssertHelpers.JsonEqual(expectedString, actualString); }
private static void AssertVariationIndexFromRollout( int expectedVariation, Rollout rollout, User user, string flagKey, string salt ) { var flag1 = new FeatureFlagBuilder(flagKey) .On(true) .GeneratedVariations(3) .FallthroughRollout(rollout) .Salt(salt) .Build(); var result1 = BasicEvaluator.Evaluate(flag1, user, EventFactory.Default); Assert.Equal(EvaluationReason.FallthroughReason, result1.Result.Reason); Assert.Equal(expectedVariation, result1.Result.VariationIndex); // Make sure we consistently apply the rollout regardless of whether it's in a rule or a fallthrough var flag2 = new FeatureFlagBuilder(flagKey) .On(true) .GeneratedVariations(3) .Rules(new RuleBuilder().Id("id") .Rollout(rollout) .Clauses(new ClauseBuilder().Attribute(UserAttribute.Key).Op(Operator.In).Values(LdValue.Of(user.Key)).Build()) .Build()) .Salt(salt) .Build(); var result2 = BasicEvaluator.Evaluate(flag2, user, EventFactory.Default); Assert.Equal(EvaluationReason.RuleMatchReason(0, "id"), result2.Result.Reason); Assert.Equal(expectedVariation, result2.Result.VariationIndex); }
public void BigSegmentStateIsQueriedOnlyOncePerUserEvenIfFlagReferencesMultipleSegments() { var segment1 = new SegmentBuilder("segmentkey1").Unbounded(true).Generation(2).Build(); var segment2 = new SegmentBuilder("segmentkey2").Unbounded(true).Generation(3).Build(); var bigSegments = new MockBigSegmentProvider(); var membership = MockMembership.New().Include(segment2); bigSegments.Membership[baseUser.Key] = membership; var flag = new FeatureFlagBuilder("key").On(true) .Variations(LdValue.Of(false), LdValue.Of(true)) .FallthroughVariation(0) .Rules( new RuleBuilder().Variation(1).Clauses(ClauseBuilder.ShouldMatchSegment(segment1.Key)).Build(), new RuleBuilder().Variation(1).Clauses(ClauseBuilder.ShouldMatchSegment(segment2.Key)).Build() ) .Build(); var evaluator = BasicEvaluator.WithStoredSegments(segment1, segment2).WithBigSegments(bigSegments); var result = evaluator.Evaluate(flag, baseUser, EventFactory.Default); Assert.Equal(LdValue.Of(true), result.Result.Value); Assert.Equal(BigSegmentsStatus.Healthy, result.Result.Reason.BigSegmentsStatus); Assert.Equal(1, bigSegments.MembershipQueryCount); Assert.Equal(new List <string> { MakeBigSegmentRef(segment1), MakeBigSegmentRef(segment2) }, membership.Queries); }
public void ClauseCanMatchCustomAttribute() { var clause = new ClauseBuilder().Attribute("legs").Op("in").Values(LdValue.Of(4)).Build(); var f = new FeatureFlagBuilder("key").BooleanWithClauses(clause).Build(); var user = User.Builder("key").Custom("legs", 4).Build(); Assert.Equal(LdValue.Of(true), BasicEvaluator.Evaluate(f, user, EventFactory.Default).Result.Value); }
public void ClauseReturnsFalseForMissingAttribute() { var clause = new ClauseBuilder().Attribute("legs").Op("in").Values(LdValue.Of(4)).Build(); var f = new FeatureFlagBuilder("key").BooleanWithClauses(clause).Build(); var user = User.Builder("key").Name("bob").Build(); Assert.Equal(LdValue.Of(false), BasicEvaluator.Evaluate(f, user, EventFactory.Default).Result.Value); }
public void SegmentMatchClauseFallsThroughIfSegmentNotFound() { var f = new FeatureFlagBuilder("key").BooleanMatchingSegment("segkey").Build(); var user = User.WithKey("foo"); var evaluator = BasicEvaluator.WithNonexistentSegment("segkey"); Assert.Equal(LdValue.Of(false), evaluator.Evaluate(f, user, EventFactory.Default).Result.Value); }
private bool SegmentMatchesUser(Segment segment, User user) { var flag = new FeatureFlagBuilder("key").BooleanMatchingSegment(segment.Key).Build(); var evaluator = BasicEvaluator.WithStoredSegments(segment); var result = evaluator.Evaluate(flag, user, EventFactory.Default); return(result.Result.Value.AsBool); }
public void ClauseWithUnknownOperatorDoesNotMatch() { var clause = new ClauseBuilder().Attribute("name").Op("invalidOp").Values(LdValue.Of("Bob")).Build(); var f = new FeatureFlagBuilder("key").BooleanWithClauses(clause).Build(); var user = User.Builder("key").Name("Bob").Build(); Assert.Equal(LdValue.Of(false), BasicEvaluator.Evaluate(f, user, EventFactory.Default).Result.Value); }
public void ClauseCanBeNegated() { var clause = new ClauseBuilder().Attribute("name").Op("in").Values(LdValue.Of("Bob")) .Negate(true).Build(); var f = new FeatureFlagBuilder("key").BooleanWithClauses(clause).Build(); var user = User.Builder("key").Name("Bob").Build(); Assert.Equal(LdValue.Of(false), BasicEvaluator.Evaluate(f, user, EventFactory.Default).Result.Value); }
public void StringVariationSendsEvent() { var flag = new FeatureFlagBuilder("key").Version(1).OffWithValue(LdValue.Of("b")).Build(); testData.UsePreconfiguredFlag(flag); client.StringVariation("key", user, "a"); Assert.Equal(1, eventSink.Events.Count); CheckFeatureEvent(eventSink.Events[0], flag, LdValue.Of("b"), LdValue.Of("a"), null); }
public void SegmentMatchClauseRetrievesSegmentFromStore() { var segment = new SegmentBuilder("segkey").Version(1).Included("foo").Build(); var evaluator = BasicEvaluator.WithStoredSegments(segment); var f = new FeatureFlagBuilder("key").BooleanMatchingSegment("segkey").Build(); var user = User.WithKey("foo"); Assert.Equal(LdValue.Of(true), evaluator.Evaluate(f, user, EventFactory.Default).Result.Value); }
public void SerializeFlagWithMinimalProperties() { var flag = new FeatureFlagBuilder() .Version(1) .Value(LdValue.Of(false)) .Build(); AssertJsonEqual(@"{""version"":1,""value"":false}", DataModelSerialization.SerializeFlag(flag)); }
public void FloatVariationSendsEvent() { var flag = new FeatureFlagBuilder("key").Version(1).OffWithValue(LdValue.Of(2.5f)).Build(); testData.UsePreconfiguredFlag(flag); client.FloatVariation("key", user, 1.0f); Assert.Single(eventSink.Events); CheckFeatureEvent(eventSink.Events[0], flag, LdValue.Of(2.5f), LdValue.Of(1.0f), null); }
public async Task RemovesUser() { string savedJson = null; var flags = new FeatureFlagBuilder() .WithFlight("A", accounts: new List <string> { "user1", "user2" }) .WithFlight("B", accounts: new List <string> { "USER1" }) .WithFlight("C", accounts: new List <string> { "user2" }) .Build(); _storage .Setup(s => s.GetFileReferenceAsync(CoreConstants.Folders.ContentFolderName, CoreConstants.FeatureFlagsFileName, null)) .ReturnsAsync(BuildFileReference(flags)); _storage .Setup(s => s.SaveFileAsync( CoreConstants.Folders.ContentFolderName, CoreConstants.FeatureFlagsFileName, It.IsAny <Stream>(), It.IsAny <IAccessCondition>())) .Callback((string folder, string file, Stream content, IAccessCondition condition) => { savedJson = ToString(content); }) .Returns(Task.CompletedTask); // Act await _target.RemoveUserAsync(new User { Username = "******" }); // Arrange Assert.NotNull(savedJson); var savedFlags = JsonConvert.DeserializeObject <FeatureFlags>(savedJson); Assert.Equal(3, savedFlags.Flights.Count); Assert.Single(savedFlags.Flights["A"].Accounts); Assert.Empty(savedFlags.Flights["B"].Accounts); Assert.Single(savedFlags.Flights["C"].Accounts); Assert.Equal("user2", savedFlags.Flights["A"].Accounts[0]); Assert.Equal("user2", savedFlags.Flights["C"].Accounts[0]); _storage.Verify( s => s.SaveFileAsync( CoreConstants.Folders.ContentFolderName, CoreConstants.FeatureFlagsFileName, It.IsAny <Stream>(), It.IsAny <IAccessCondition>()), Times.Once); }
public void GetKnownFlag() { var flag1 = new FeatureFlagBuilder().Version(100).Value(LdValue.Of(true)).Build(); var initData = new DataSetBuilder() .Add("flag1", flag1) .Build(); _store.Init(BasicUser, initData, false); Assert.Equal(flag1.ToItemDescriptor(), _store.Get("flag1")); }
public void AllFlagsStateReturnsEmptyStateForUserWithNullKey() { var flag = new FeatureFlagBuilder("key1").OffWithValue(LdValue.Of("value1")).Build(); testData.UsePreconfiguredFlag(flag); var state = client.AllFlagsState(User.WithKey(null)); Assert.False(state.Valid); Assert.Equal(0, state.ToValuesJsonMap().Count); }
public void JsonVariationSendsEvent() { var data = LdValue.BuildObject().Add("thing", "stuff").Build(); var flag = new FeatureFlagBuilder("key").Version(1).OffWithValue(data).Build(); testData.UsePreconfiguredFlag(flag); var defaultVal = LdValue.Of(42); client.JsonVariation("key", user, defaultVal); Assert.Equal(1, eventSink.Events.Count); CheckFeatureEvent(eventSink.Events[0], flag, data, defaultVal, null); }
public async Task WhenSavePreconditionFailsOnce_Retries() { // Arrange var firstTry = true; string savedJson = null; var flags = new FeatureFlagBuilder() .WithFlight("A", accounts: new List <string> { "user1", "user2" }) .Build(); _storage .Setup(s => s.GetFileReferenceAsync(CoreConstants.Folders.ContentFolderName, CoreConstants.FeatureFlagsFileName, null)) .ReturnsAsync(BuildFileReference(flags)); _storage .Setup(s => s.SaveFileAsync( CoreConstants.Folders.ContentFolderName, CoreConstants.FeatureFlagsFileName, It.IsAny <Stream>(), It.IsAny <IAccessCondition>())) .Callback((string folder, string file, Stream content, IAccessCondition condition) => { if (firstTry) { firstTry = false; throw _preconditionException; } savedJson = ToString(content); }) .Returns(Task.CompletedTask); // Act await _target.RemoveUserAsync(new User { Username = "******" }); // Assert Assert.NotNull(savedJson); var savedFlags = JsonConvert.DeserializeObject <FeatureFlags>(savedJson); Assert.Single(savedFlags.Flights); Assert.Single(savedFlags.Flights["A"].Accounts); Assert.Equal("user2", savedFlags.Flights["A"].Accounts[0]); _storage.Verify( s => s.SaveFileAsync( CoreConstants.Folders.ContentFolderName, CoreConstants.FeatureFlagsFileName, It.IsAny <Stream>(), It.IsAny <IAccessCondition>()), Times.Exactly(2)); }
public async Task WhenSavePreconditionAlwaysFails_Throws() { // Arrange var flags = new FeatureFlagBuilder() .WithFlight("A", accounts: new List <string> { "user1", "user2" }) .Build(); _storage .Setup(s => s.GetFileReferenceAsync(CoreConstants.Folders.ContentFolderName, CoreConstants.FeatureFlagsFileName, null)) .ReturnsAsync(BuildFileReference(flags)); _storage .Setup(s => s.SaveFileAsync( CoreConstants.Folders.ContentFolderName, CoreConstants.FeatureFlagsFileName, It.IsAny <Stream>(), It.IsAny <IAccessCondition>())) .ThrowsAsync(_preconditionException); // Act var exception = await Assert.ThrowsAsync <InvalidOperationException>(() => _target.RemoveUserAsync(new User { Username = "******" })); // Assert Assert.Contains("Unable to remove user from feature flags", exception.Message); _storage.Verify( s => s.SaveFileAsync( CoreConstants.Folders.ContentFolderName, CoreConstants.FeatureFlagsFileName, It.IsAny <Stream>(), It.IsAny <IAccessCondition>()), Times.Exactly(3)); _auditing.Verify( a => a.SaveAuditRecordAsync( It.Is <FeatureFlagsAuditRecord>( r => r.Action == AuditedFeatureFlagsAction.Update && r.ContentId == "fake-content-id" && r.Result == FeatureFlagSaveResult.Conflict && !r.Features.Any() && r.Flights.SingleOrDefault( f => f.Name == "A" && !f.All && !f.SiteAdmins && f.Accounts.Single() == "user2" && !f.Domains.Any()) != null)), Times.Exactly(3)); }
public void FlagValueChangeListener() { var flagKey = "important-flag"; var user = User.WithKey("important-user"); var otherUser = User.WithKey("unimportant-user"); var store = new InMemoryDataStore(); var dataSourceUpdates = TestUtils.BasicDataSourceUpdates(store, testLogger); var resultMap = new Dictionary <KeyValuePair <string, User>, LdValue>(); var tracker = new FlagTrackerImpl(dataSourceUpdates, (key, u) => resultMap[new KeyValuePair <string, User>(key, u)]); resultMap[new KeyValuePair <string, User>(flagKey, user)] = LdValue.Of(false); resultMap[new KeyValuePair <string, User>(flagKey, otherUser)] = LdValue.Of(false); var eventSink1 = new EventSink <FlagValueChangeEvent>(); var eventSink2 = new EventSink <FlagValueChangeEvent>(); var eventSink3 = new EventSink <FlagValueChangeEvent>(); var listener1 = tracker.FlagValueChangeHandler(flagKey, user, eventSink1.Add); var listener2 = tracker.FlagValueChangeHandler(flagKey, user, eventSink2.Add); var listener3 = tracker.FlagValueChangeHandler(flagKey, otherUser, eventSink3.Add); tracker.FlagChanged += listener1; tracker.FlagChanged += listener2; tracker.FlagChanged -= listener2; // just verifying that removing a listener works tracker.FlagChanged += listener3; eventSink1.ExpectNoValue(); eventSink2.ExpectNoValue(); eventSink3.ExpectNoValue(); // make the flag true for the first user only, and broadcast a flag change event resultMap[new KeyValuePair <string, User>(flagKey, user)] = LdValue.Of(true); var flagV1 = new FeatureFlagBuilder(flagKey).Version(1).Build(); dataSourceUpdates.Upsert(DataModel.Features, flagKey, DescriptorOf(flagV1)); // eventSink1 receives a value change event var event1 = eventSink1.ExpectValue(); Assert.Equal(flagKey, event1.Key); Assert.Equal(LdValue.Of(false), event1.OldValue); Assert.Equal(LdValue.Of(true), event1.NewValue); eventSink1.ExpectNoValue(); // eventSink2 doesn't receive one, because it was unregistered eventSink2.ExpectNoValue(); // eventSink3 doesn't receive one, because the flag's value hasn't changed for otherUser eventSink3.ExpectNoValue(); }