-
-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Created FeatureFlagging services, APIs and source generator, and Flag…
…smith adapter, with stubs, and docs. #4
- Loading branch information
1 parent
004141b
commit 91e8ac4
Showing
129 changed files
with
3,739 additions
and
119 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
8 changes: 8 additions & 0 deletions
8
src/.idea/.idea.SaaStack/.idea/inspectionProfiles/Project_Default.xml
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
85 changes: 85 additions & 0 deletions
85
src/AncillaryApplication.UnitTests/FeatureFlagsApplicationSpec.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>())); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
62 changes: 62 additions & 0 deletions
62
src/AncillaryInfrastructure.IntegrationTests/FeatureFlagsApiSpec.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
|
||
{ | ||
#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
|
||
{ | ||
#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 | ||
} | ||
} |
Oops, something went wrong.