Skip to content

Commit

Permalink
Adds exclude logic to Targeting and Audience, adds unit test, updates…
Browse files Browse the repository at this point in the history
… example to include exclude (#218)

* Adds exclude logic to Targeting and Audience, adds unit test, updates example to include exclusion. Adds ComparerType for .Contains string comparison

---------

Co-authored-by: Avani Gupta <avanigupta@users.noreply.github.com>
  • Loading branch information
rossgrambo and avanigupta authored Mar 24, 2023
1 parent 8e8620f commit 1fedf2c
Show file tree
Hide file tree
Showing 8 changed files with 205 additions and 2 deletions.
13 changes: 13 additions & 0 deletions examples/TargetingConsoleApp/Identity/InMemoryUserRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,19 @@ class InMemoryUserRepository : IUserRepository
"TeamMembers"
}
},
new User
{
Id = "Anne",
Groups = Enumerable.Empty<string>()
},
new User
{
Id = "Chuck",
Groups = new List<string>()
{
"Contractor"
}
},
};

public Task<User> GetUser(string id)
Expand Down
14 changes: 12 additions & 2 deletions examples/TargetingConsoleApp/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"Parameters": {
"Audience": {
"Users": [
"Jeff"
"Jeff",
"Anne"
],
"Groups": [
{
Expand All @@ -19,7 +20,16 @@
"RolloutPercentage": 45
}
],
"DefaultRolloutPercentage": 20
"DefaultRolloutPercentage": 20,
"Exclusion": {
"Users": [
"Anne",
"Phil"
],
"Groups": [
"Contractor"
]
}
}
}
}
Expand Down
5 changes: 5 additions & 0 deletions src/Microsoft.FeatureManagement/Targeting/Audience.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,10 @@ public class Audience
/// Includes users in the audience based off a percentage of the total possible audience. Valid values range from 0 to 100 inclusive.
/// </summary>
public double DefaultRolloutPercentage { get; set; }

/// <summary>
/// Excludes a basic audience from this audience.
/// </summary>
public BasicAudience Exclusion { get; set; }
}
}
23 changes: 23 additions & 0 deletions src/Microsoft.FeatureManagement/Targeting/BasicAudience.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
using System.Collections.Generic;

namespace Microsoft.FeatureManagement.FeatureFilters
{
/// <summary>
/// A basic audience definition describing a set of users and groups.
/// </summary>
public class BasicAudience
{
/// <summary>
/// Includes users in the audience by name.
/// </summary>
public List<string> Users { get; set; }

/// <summary>
/// Includes users in the audience by group name.
/// </summary>
public List<string> Groups { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
Expand Down Expand Up @@ -34,6 +35,7 @@ public ContextualTargetingFilter(IOptions<TargetingEvaluationOptions> options, I
}

private StringComparison ComparisonType => _options.IgnoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
private StringComparer ComparerType => _options.IgnoreCase ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal;

/// <summary>
/// Performs a targeting evaluation using the provided <see cref="TargetingContext"/> to determine if a feature should be enabled.
Expand Down Expand Up @@ -61,6 +63,27 @@ public Task<bool> EvaluateAsync(FeatureFilterEvaluationContext context, ITargeti
throw new ArgumentException(message, paramName);
}

if (settings.Audience.Exclusion != null)
{
//
// Check if the user is in the exclusion directly
if (targetingContext.UserId != null &&
settings.Audience.Exclusion.Users != null &&
settings.Audience.Exclusion.Users.Any(user => targetingContext.UserId.Equals(user, ComparisonType)))
{
return Task.FromResult(false);
}

//
// Check if the user is in a group within exclusion
if (targetingContext.Groups != null &&
settings.Audience.Exclusion.Groups != null &&
settings.Audience.Exclusion.Groups.Any(group => targetingContext.Groups.Contains(group, ComparerType)))
{
return Task.FromResult(false);
}
}

//
// Check if the user is being targeted directly
if (targetingContext.UserId != null &&
Expand Down
93 changes: 93 additions & 0 deletions tests/Tests.FeatureManagement/FeatureManagement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,99 @@ public async Task ThreadsafeSnapshot()
}
}

[Fact]
public async Task TargetingExclusion()
{
IConfiguration config = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build();

var services = new ServiceCollection();

services
.Configure<FeatureManagementOptions>(options =>
{
options.IgnoreMissingFeatureFilters = true;
});

services
.AddSingleton(config)
.AddFeatureManagement()
.AddFeatureFilter<ContextualTargetingFilter>();

ServiceProvider serviceProvider = services.BuildServiceProvider();

IFeatureManager featureManager = serviceProvider.GetRequiredService<IFeatureManager>();

string targetingTestFeature = Enum.GetName(typeof(Features), Features.TargetingTestFeatureWithExclusion);

//
// Targeted by user id
Assert.True(await featureManager.IsEnabledAsync(targetingTestFeature, new TargetingContext
{
UserId = "Alicia"
}));

//
// Not targeted by user id, but targeted by default rollout
Assert.True(await featureManager.IsEnabledAsync(targetingTestFeature, new TargetingContext
{
UserId = "Anne"
}));

//
// Not targeted by user id or default rollout
Assert.False(await featureManager.IsEnabledAsync(targetingTestFeature, new TargetingContext
{
UserId = "Patty"
}));

//
// Targeted by group rollout
Assert.True(await featureManager.IsEnabledAsync(targetingTestFeature, new TargetingContext
{
UserId = "Patty",
Groups = new List<string>() { "Ring1" }
}));

//
// Not targeted by user id, default rollout or group rollout
Assert.False(await featureManager.IsEnabledAsync(targetingTestFeature, new TargetingContext
{
UserId = "Isaac",
Groups = new List<string>() { "Ring1" }
}));

//
// Excluded by user id
Assert.False(await featureManager.IsEnabledAsync(targetingTestFeature, new TargetingContext
{
UserId = "Jeff"
}));

//
// Excluded by group
Assert.False(await featureManager.IsEnabledAsync(targetingTestFeature, new TargetingContext
{
UserId = "Patty",
Groups = new List<string>() { "Ring0" }
}));

//
// Included and Excluded by group
Assert.False(await featureManager.IsEnabledAsync(targetingTestFeature, new TargetingContext
{
UserId = "Patty",
Groups = new List<string>() { "Ring0", "Ring1" }
}));

//
// Included user but Excluded by group
Assert.False(await featureManager.IsEnabledAsync(targetingTestFeature, new TargetingContext
{
UserId = "Alicia",
Groups = new List<string>() { "Ring2" }
}));
}

private static void DisableEndpointRouting(MvcOptions options)
{
#if NET6_0 || NET5_0 || NETCOREAPP3_1
Expand Down
1 change: 1 addition & 0 deletions tests/Tests.FeatureManagement/Features.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ namespace Tests.FeatureManagement
enum Features
{
TargetingTestFeature,
TargetingTestFeatureWithExclusion,
OnTestFeature,
OffTestFeature,
ConditionalFeature,
Expand Down
35 changes: 35 additions & 0 deletions tests/Tests.FeatureManagement/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,41 @@
}
]
},
"TargetingTestFeatureWithExclusion": {
"EnabledFor": [
{
"Name": "Targeting",
"Parameters": {
"Audience": {
"Users": [
"Jeff",
"Alicia"
],
"Groups": [
{
"Name": "Ring0",
"RolloutPercentage": 100
},
{
"Name": "Ring1",
"RolloutPercentage": 50
}
],
"DefaultRolloutPercentage": 20,
"Exclusion": {
"Users": [
"Jeff"
],
"Groups": [
"Ring0",
"Ring2"
]
}
}
}
}
]
},
"ConditionalFeature": {
"EnabledFor": [
{
Expand Down

0 comments on commit 1fedf2c

Please sign in to comment.