-
-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
…smith adapter, with stubs, and docs. #4
- Loading branch information
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
# Feature Flagging | ||
|
||
## Design Principles | ||
|
||
* We want to be able to deploy code which includes features/code that we dont want visible/available/enabled for end users. | ||
* We want to be able to progressively roll-out certain features to specific users, or segments of the market to manage any risk of deploying new features | ||
* This optionality can be attributed to all end-users, or specific end-users, or even all users within a specific tenant | ||
* We want those features to be configured externally to the running system, without changing what has been deployed | ||
* We want to have those flags managed separately to our system, so that we don't have to build this kind of infrastructure ourselves | ||
|
||
## Implementation | ||
|
||
We have provided a service called `IFeatureFlags` that is available in any component of the architecture. | ||
|
||
> That service will be implemented by an adapter to a 3rd party external system such as FlagSmith, GitLab, LaunchDarkly etc. | ||
We have also provided an API to access this capability from the BEFFE, so flags can be shared in the Frontend JS app. | ||
|
||
The interface `IFeatureFlags` provides methods to query flags in the system, using pre-defined flags in the code, that should be represented in the 3rd party system. | ||
|
||
For example, | ||
|
||
```c# | ||
public class MyClass | ||
{ | ||
private readonly IFeatureFlags _featureFlags; | ||
|
||
public MyClass(IFeatureFlags featureFlags) | ||
{ | ||
_featureFlags = featureFlags; | ||
} | ||
|
||
public void DoSomethingForAllUsers() | ||
{ | ||
if (_featureFlags.IsEnabled(Flag.MyFeature)) | ||
{ | ||
...do somethign with this feature | ||
} | ||
} | ||
|
||
public void DoSomethingForTheCallerUser(ICallerContext caller) | ||
{ | ||
if (_featureFlags.IsEnabled(Flag.MyFeature, caller)) | ||
{ | ||
...do somethign with this feature | ||
} | ||
} | ||
} | ||
``` | ||
|
||
Where `MyFeature` is defined as a flag in `FeatureFlags.resx` file in the `Common` project. | ||
|
||
### Defining flags | ||
|
||
In code, flags are defined in the `FeatureFlags.resx` file in the `Common` project. | ||
|
||
A source generator runs every build to translate those entries in the resource file to instances of the `Flags` class, to provide a typed collection of flags for use in code. | ||
|
||
> This provides an easy way for intellisense to offer you the possible flags in the codebase to avoid using flags that no longer exist. |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
using Application.Interfaces; | ||
using Common; | ||
using Common.FeatureFlags; | ||
using FluentAssertions; | ||
using Moq; | ||
using UnitTesting.Common; | ||
using Xunit; | ||
|
||
namespace AncillaryApplication.UnitTests; | ||
|
||
[Trait("Category", "Unit")] | ||
public class FeatureFlagsApplicationSpec | ||
{ | ||
private readonly FeatureFlagsApplication _application; | ||
private readonly Mock<ICallerContext> _caller; | ||
private readonly Mock<IFeatureFlags> _featuresService; | ||
|
||
public FeatureFlagsApplicationSpec() | ||
{ | ||
var recorder = new Mock<IRecorder>(); | ||
_caller = new Mock<ICallerContext>(); | ||
_caller.Setup(cc => cc.IsAuthenticated).Returns(true); | ||
_caller.Setup(cc => cc.CallerId).Returns("acallerid"); | ||
_caller.Setup(cc => cc.TenantId).Returns("atenantid"); | ||
_featuresService = new Mock<IFeatureFlags>(); | ||
_featuresService.Setup(fs => fs.GetFlagAsync(It.IsAny<Flag>(), It.IsAny<Optional<string>>(), | ||
It.IsAny<Optional<string>>(), It.IsAny<CancellationToken>())) | ||
.ReturnsAsync(new FeatureFlag | ||
{ | ||
Name = "aname", | ||
IsEnabled = true | ||
}); | ||
_application = new FeatureFlagsApplication(recorder.Object, _featuresService.Object); | ||
} | ||
|
||
[Fact] | ||
public async Task WhenGetFeatureFlag_ThenReturns() | ||
{ | ||
var result = | ||
await _application.GetFeatureFlagAsync(_caller.Object, "aname", null, "auserid", CancellationToken.None); | ||
|
||
result.Should().BeSuccess(); | ||
result.Value.Name.Should().Be("aname"); | ||
result.Value.IsEnabled.Should().BeTrue(); | ||
_featuresService.Verify(fs => fs.GetFlagAsync(It.Is<Flag>(flag => flag.Name == "aname"), Optional<string>.None, | ||
"auserid", It.IsAny<CancellationToken>())); | ||
} | ||
|
||
[Fact] | ||
public async Task WhenGetFeatureFlagForCaller_ThenReturns() | ||
{ | ||
var result = | ||
await _application.GetFeatureFlagForCallerAsync(_caller.Object, "aname", CancellationToken.None); | ||
|
||
result.Should().BeSuccess(); | ||
result.Value.Name.Should().Be("aname"); | ||
result.Value.IsEnabled.Should().BeTrue(); | ||
_featuresService.Verify(fs => fs.GetFlagAsync(It.Is<Flag>(flag => | ||
flag.Name == "aname" | ||
), "atenantid", "acallerid", It.IsAny<CancellationToken>())); | ||
} | ||
|
||
[Fact] | ||
public async Task WhenGetAllFeatureFlags_ThenReturns() | ||
{ | ||
_featuresService.Setup(fs => fs.GetAllFlagsAsync(It.IsAny<CancellationToken>())) | ||
.ReturnsAsync(new List<FeatureFlag> | ||
{ | ||
new() | ||
{ | ||
Name = "aname", | ||
IsEnabled = true | ||
} | ||
}); | ||
|
||
var result = | ||
await _application.GetAllFeatureFlagsAsync(_caller.Object, CancellationToken.None); | ||
|
||
result.Should().BeSuccess(); | ||
result.Value.Count.Should().Be(1); | ||
result.Value[0].Name.Should().Be("aname"); | ||
result.Value[0].IsEnabled.Should().BeTrue(); | ||
_featuresService.Verify(fs => fs.GetAllFlagsAsync(It.IsAny<CancellationToken>())); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
using Application.Interfaces; | ||
using Common; | ||
using Common.FeatureFlags; | ||
|
||
namespace AncillaryApplication; | ||
|
||
public interface IFeatureFlagsApplication | ||
{ | ||
Task<Result<List<FeatureFlag>, Error>> GetAllFeatureFlagsAsync(ICallerContext context, | ||
CancellationToken cancellationToken); | ||
|
||
Task<Result<FeatureFlag, Error>> GetFeatureFlagAsync(ICallerContext context, string name, string? tenantId, | ||
string userId, CancellationToken cancellationToken); | ||
|
||
Task<Result<FeatureFlag, Error>> GetFeatureFlagForCallerAsync(ICallerContext context, string name, | ||
CancellationToken cancellationToken); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
using System.Net; | ||
using ApiHost1; | ||
using Common.FeatureFlags; | ||
using FluentAssertions; | ||
using Infrastructure.Web.Api.Common.Extensions; | ||
using Infrastructure.Web.Api.Operations.Shared.Ancillary; | ||
using IntegrationTesting.WebApi.Common; | ||
using IntegrationTesting.WebApi.Common.Stubs; | ||
using Microsoft.Extensions.DependencyInjection; | ||
using Xunit; | ||
using Task = System.Threading.Tasks.Task; | ||
|
||
namespace AncillaryInfrastructure.IntegrationTests; | ||
|
||
[Trait("Category", "Integration.Web")] | ||
[Collection("API")] | ||
public class FeatureFlagsApiSpec : WebApiSpec<Program> | ||
{ | ||
private readonly StubFeatureFlags _featureFlags; | ||
|
||
public FeatureFlagsApiSpec(WebApiSetup<Program> setup) : base(setup, OverrideDependencies) | ||
{ | ||
EmptyAllRepositories(); | ||
_featureFlags = setup.GetRequiredService<IFeatureFlags>().As<StubFeatureFlags>(); | ||
_featureFlags.Reset(); | ||
} | ||
|
||
[Fact] | ||
public async Task WhenGetAllFeatureFlags_ThenReturnsFlags() | ||
Check warning on line 29 in src/AncillaryInfrastructure.IntegrationTests/FeatureFlagsApiSpec.cs GitHub Actions / build
Check warning on line 29 in src/AncillaryInfrastructure.IntegrationTests/FeatureFlagsApiSpec.cs GitHub Actions / build
|
||
{ | ||
#if TESTINGONLY | ||
var request = new GetAllFeatureFlagsRequest(); | ||
|
||
var result = await Api.GetAsync(request, req => req.SetHMACAuth(request, "asecret")); | ||
|
||
result.StatusCode.Should().Be(HttpStatusCode.OK); | ||
result.Content.Value.Flags.Count.Should().Be(0); | ||
#endif | ||
} | ||
|
||
[Fact] | ||
public async Task WhenGetFeatureFlag_ThenReturnsFlag() | ||
Check warning on line 42 in src/AncillaryInfrastructure.IntegrationTests/FeatureFlagsApiSpec.cs GitHub Actions / build
Check warning on line 42 in src/AncillaryInfrastructure.IntegrationTests/FeatureFlagsApiSpec.cs GitHub Actions / build
|
||
{ | ||
#if TESTINGONLY | ||
var request = new GetFeatureFlagForCallerRequest | ||
{ | ||
Name = Flag.TestingOnly.Name | ||
}; | ||
|
||
var result = await Api.GetAsync(request, req => req.SetHMACAuth(request, "asecret")); | ||
|
||
result.StatusCode.Should().Be(HttpStatusCode.OK); | ||
result.Content.Value.Flag!.Name.Should().Be(Flag.TestingOnly.Name); | ||
_featureFlags.LastGetFlag.Should().Be(Flag.TestingOnly.Name); | ||
#endif | ||
} | ||
|
||
private static void OverrideDependencies(IServiceCollection services) | ||
{ | ||
// nothing here yet | ||
} | ||
} |