-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add IFeatureService
& Related Utilities
#37
Changes from all commits
335c51a
ae3cf97
49cf70c
8956cdb
96d17ad
af18b29
86a3888
4e4208d
aedfad4
9b2c6c4
6c4ac01
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,21 @@ | ||
using Bitwarden.Extensions.Hosting.Features; | ||
|
||
var builder = WebApplication.CreateBuilder(args); | ||
|
||
builder.UseBitwardenDefaults(); | ||
|
||
var app = builder.Build(); | ||
|
||
app.UseRouting(); | ||
|
||
app.UseFeatureFlagChecks(); | ||
|
||
app.MapGet("/", (IConfiguration config) => ((IConfigurationRoot)config).GetDebugView()); | ||
|
||
app.MapGet("/requires-feature", (IFeatureService featureService) => | ||
{ | ||
return featureService.GetAll(); | ||
}) | ||
.RequireFeature("feature-one"); | ||
|
||
app.Run(); |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
using Bitwarden.Extensions.Hosting.Features; | ||
|
||
namespace Microsoft.AspNetCore.Builder; | ||
|
||
/// <summary> | ||
/// Feature extension methods for <see cref="IApplicationBuilder"/>. | ||
/// </summary> | ||
public static class FeatureApplicationBuilderExtensions | ||
{ | ||
/// <summary> | ||
/// Adds the <see cref="FeatureCheckMiddleware"/> to the specified <see cref="IApplicationBuilder"/>, which enabled feature check capabilities. | ||
/// <para> | ||
/// This call must take place between <c>app.UseRouting()</c> and <c>app.UseEndpoints(...)</c> for middleware to function properly. | ||
/// </para> | ||
/// </summary> | ||
/// <param name="app">The <see cref="IApplicationBuilder"/> to add the middleware to.</param> | ||
/// <returns>A reference to <paramref name="app"/> after the operation has completed.</returns> | ||
public static IApplicationBuilder UseFeatureFlagChecks(this IApplicationBuilder app) | ||
{ | ||
ArgumentNullException.ThrowIfNull(app); | ||
|
||
// This would be a good time to make sure that IFeatureService is registered but it is a scoped service | ||
// and I don't think creating a scope is worth it for that. If we think this is a problem we can add another | ||
// marker interface that is registered as a singleton and validate that it exists here. | ||
|
||
app.UseMiddleware<FeatureCheckMiddleware>(); | ||
return app; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
using Microsoft.AspNetCore.Http; | ||
using Microsoft.AspNetCore.Mvc; | ||
using Microsoft.Extensions.Hosting; | ||
using Microsoft.Extensions.Logging; | ||
|
||
namespace Bitwarden.Extensions.Hosting.Features; | ||
|
||
internal sealed class FeatureCheckMiddleware | ||
{ | ||
private readonly RequestDelegate _next; | ||
private readonly IHostEnvironment _hostEnvironment; | ||
private readonly IProblemDetailsService _problemDetailsService; | ||
private readonly ILogger<FeatureCheckMiddleware> _logger; | ||
|
||
public FeatureCheckMiddleware( | ||
RequestDelegate next, | ||
IHostEnvironment hostEnvironment, | ||
IProblemDetailsService problemDetailsService, | ||
ILogger<FeatureCheckMiddleware> logger) | ||
{ | ||
ArgumentNullException.ThrowIfNull(next); | ||
ArgumentNullException.ThrowIfNull(hostEnvironment); | ||
ArgumentNullException.ThrowIfNull(problemDetailsService); | ||
ArgumentNullException.ThrowIfNull(logger); | ||
|
||
_next = next; | ||
_hostEnvironment = hostEnvironment; | ||
_problemDetailsService = problemDetailsService; | ||
_logger = logger; | ||
} | ||
|
||
public Task Invoke(HttpContext context, IFeatureService featureService) | ||
{ | ||
// This middleware is expected to be placed after `UseRouting()` which will fill in this endpoint | ||
var endpoint = context.GetEndpoint(); | ||
|
||
if (endpoint == null) | ||
{ | ||
_logger.LogNoEndpointWarning(); | ||
return _next(context); | ||
} | ||
|
||
var featureMetadatas = endpoint.Metadata.GetOrderedMetadata<IFeatureMetadata>(); | ||
|
||
foreach (var featureMetadata in featureMetadatas) | ||
{ | ||
if (!featureMetadata.FeatureCheck(featureService)) | ||
{ | ||
// Do not execute more of the pipeline, return early. | ||
return HandleFailedFeatureCheck(context, featureMetadata); | ||
} | ||
|
||
// Continue checking | ||
} | ||
|
||
// Either there were no feature checks, or none were failed. Continue on in the pipeline. | ||
return _next(context); | ||
} | ||
|
||
private async Task HandleFailedFeatureCheck(HttpContext context, IFeatureMetadata failedFeature) | ||
{ | ||
if (_logger.IsEnabled(LogLevel.Debug)) | ||
{ | ||
_logger.LogFailedFeatureCheck(failedFeature.ToString()!); | ||
} | ||
Check warning on line 65 in extensions/Bitwarden.Extensions.Hosting/src/Features/FeatureCheckMiddleware.cs Codecov / codecov/patchextensions/Bitwarden.Extensions.Hosting/src/Features/FeatureCheckMiddleware.cs#L63-L65
|
||
|
||
context.Response.StatusCode = StatusCodes.Status404NotFound; | ||
|
||
var problemDetails = new ProblemDetails(); | ||
problemDetails.Title = "Resource not found."; | ||
problemDetails.Status = StatusCodes.Status404NotFound; | ||
|
||
// Message added for legacy reasons. We should start preferring title/detail | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. โ Legacy? So There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, a lot of things in ASP.NET Core will return a It also sends back There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, I like that. Do we just get rid of the message then? I'd like to not carry in debt and just expect callers to adjust when they adopt this library. |
||
problemDetails.Extensions["Message"] = "Resource not found."; | ||
|
||
// Follow ProblemDetails output type? Would need clients update | ||
if (_hostEnvironment.IsDevelopment()) | ||
{ | ||
// Add extra information | ||
problemDetails.Detail = $"Feature check failed: {failedFeature}"; | ||
} | ||
|
||
// We don't really care if this fails, we will return the 404 no matter what. | ||
await _problemDetailsService.TryWriteAsync(new ProblemDetailsContext | ||
{ | ||
HttpContext = context, | ||
ProblemDetails = problemDetails, | ||
// TODO: Add metadata? | ||
}); | ||
} | ||
|
||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
using Bitwarden.Extensions.Hosting.Features; | ||
|
||
namespace Microsoft.AspNetCore.Builder; | ||
|
||
/// <summary> | ||
/// Feature extension methods for <see cref="IEndpointConventionBuilder"/>. | ||
/// </summary> | ||
public static class FeatureEndpointConventionBuilderExtensions | ||
{ | ||
/// <summary> | ||
/// Adds a feature check for the given feature to the endpoint(s). | ||
/// </summary> | ||
/// <param name="builder">The endpoint convention builder.</param> | ||
/// <param name="featureNameKey"></param> | ||
/// <returns></returns> | ||
public static TBuilder RequireFeature<TBuilder>(this TBuilder builder, string featureNameKey) | ||
where TBuilder : IEndpointConventionBuilder | ||
{ | ||
ArgumentNullException.ThrowIfNull(builder); | ||
ArgumentNullException.ThrowIfNull(featureNameKey); | ||
|
||
builder.Add(endpointBuilder => | ||
{ | ||
endpointBuilder.Metadata.Add(new RequireFeatureAttribute(featureNameKey)); | ||
}); | ||
|
||
return builder; | ||
} | ||
|
||
/// <summary> | ||
/// Adds a feature check with the specified check to the endpoint(s). | ||
/// </summary> | ||
/// <param name="builder">The endpoint convention builder.</param> | ||
/// <param name="featureCheck"></param> | ||
/// <returns></returns> | ||
public static TBuilder RequireFeature<TBuilder>(this TBuilder builder, Func<IFeatureService, bool> featureCheck) | ||
where TBuilder : IEndpointConventionBuilder | ||
{ | ||
ArgumentNullException.ThrowIfNull(builder); | ||
ArgumentNullException.ThrowIfNull(featureCheck); | ||
|
||
builder.Add(endpointBuilder => | ||
{ | ||
endpointBuilder.Metadata.Add(new RequireFeatureAttribute(featureCheck)); | ||
}); | ||
|
||
return builder; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
namespace Bitwarden.Extensions.Hosting.Features; | ||
|
||
/// <summary> | ||
/// A collection of Launch Darkly specific options. | ||
/// </summary> | ||
public sealed class LaunchDarklyOptions | ||
{ | ||
/// <summary> | ||
/// The SdkKey to be used for retrieving feature flag values from Launch Darkly. | ||
/// </summary> | ||
public string? SdkKey { get; set; } | ||
} | ||
|
||
/// <summary> | ||
/// A set of options for features. | ||
/// </summary> | ||
public sealed class FeatureFlagOptions | ||
{ | ||
/// <summary> | ||
/// All the flags known to this instance, this is used to enumerable values in <see cref="IFeatureService.GetAll()"/>. | ||
/// </summary> | ||
public HashSet<string> KnownFlags { get; set; } = new HashSet<string>(); | ||
|
||
/// <summary> | ||
/// Flags and flag values to include in the feature flag data source. | ||
/// </summary> | ||
public Dictionary<string, string> FlagValues { get; set; } = new Dictionary<string, string>(); | ||
|
||
/// <summary> | ||
/// Launch Darkly specific options. | ||
/// </summary> | ||
public LaunchDarklyOptions LaunchDarkly { get; set; } = new LaunchDarklyOptions(); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
using Bitwarden.Extensions.Hosting.Features; | ||
|
||
namespace Microsoft.Extensions.DependencyInjection; | ||
|
||
/// <summary> | ||
/// Extensions for features on <see cref="IServiceCollection"/>. | ||
/// </summary> | ||
public static class ServiceCollectionExtensions | ||
{ | ||
/// <summary> | ||
/// Adds known feature flags to the <see cref="FeatureFlagOptions"/>. This makes these flags | ||
/// show up in <see cref="IFeatureService.GetAll()"/>. | ||
/// </summary> | ||
/// <param name="services">The service collection to customize the options on.</param> | ||
/// <param name="knownFlags">The flags to add to the known flags list.</param> | ||
/// <returns>The <see cref="IServiceCollection"/> to chain additional calls.</returns> | ||
public static IServiceCollection AddKnownFeatureFlags(this IServiceCollection services, IEnumerable<string> knownFlags) | ||
{ | ||
ArgumentNullException.ThrowIfNull(services); | ||
|
||
services.Configure<FeatureFlagOptions>(options => | ||
{ | ||
foreach (var flag in knownFlags) | ||
{ | ||
options.KnownFlags.Add(flag); | ||
} | ||
}); | ||
|
||
return services; | ||
} | ||
|
||
/// <summary> | ||
/// Adds feature flags and their values to the <see cref="FeatureFlagOptions"/>. | ||
/// </summary> | ||
/// <param name="services">The service collection to customize the options on.</param> | ||
/// <param name="flagValues">The flags to add to the flag values dictionary.</param> | ||
/// <returns>The <see cref="IServiceCollection"/> to chain additional calls. </returns> | ||
public static IServiceCollection AddFeatureFlagValues( | ||
this IServiceCollection services, | ||
IEnumerable<KeyValuePair<string, string>> flagValues) | ||
{ | ||
ArgumentNullException.ThrowIfNull(services); | ||
|
||
services.Configure<FeatureFlagOptions>(options => | ||
{ | ||
foreach (var flag in flagValues) | ||
{ | ||
options.FlagValues[flag.Key] = flag.Value; | ||
} | ||
}); | ||
|
||
return services; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
๐ฑ Should this example code be expected to declare known flags?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's declaring its known flags in
appsettings.Development.json
.