Skip to content

Commit

Permalink
feat: Add OpenFeature.Extensions.Hosting package
Browse files Browse the repository at this point in the history
See: open-feature/dotnet-sdk-contrib#127

Signed-off-by: Austin Drenski <[email protected]>
  • Loading branch information
austindrenski committed Jan 16, 2024
1 parent a6062fe commit c295dd0
Show file tree
Hide file tree
Showing 16 changed files with 468 additions and 15 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
7.0.x
- name: Run Tests
run: dotnet test test/OpenFeature.Tests/ --configuration Release --logger GitHubActions
run: dotnet test --logger GitHubActions

unit-tests-windows:
runs-on: windows-latest
Expand All @@ -43,4 +43,4 @@ jobs:
7.0.x
- name: Run Tests
run: dotnet test test\OpenFeature.Tests\ --configuration Release --logger GitHubActions
run: dotnet test --logger GitHubActions
10 changes: 2 additions & 8 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,13 @@ jobs:
if: ${{ steps.release.outputs.releases_created }}
run: dotnet restore

- name: Build
if: ${{ steps.release.outputs.releases_created }}
run: |
dotnet build --configuration Release --no-restore -p:Deterministic=true
- name: Pack
if: ${{ steps.release.outputs.releases_created }}
run: |
dotnet pack OpenFeature.proj --configuration Release --no-build -p:PackageID=OpenFeature
run: dotnet pack --no-restore

- name: Publish to Nuget
if: ${{ steps.release.outputs.releases_created }}
run: |
dotnet nuget push src/OpenFeature/bin/Release/OpenFeature.${{ steps.release.outputs.major }}.${{ steps.release.outputs.minor }}.${{ steps.release.outputs.patch }}.nupkg `
dotnet nuget push "src/**/*.nupkg" `
--api-key ${{secrets.NUGET_TOKEN}} `
--source https://api.nuget.org/v3/index.json
14 changes: 14 additions & 0 deletions OpenFeature.sln
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ VisualStudioVersion = 17.4.33213.308
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature", "src\OpenFeature\OpenFeature.csproj", "{07A6E6BD-FB7E-4B3B-9CBE-65AE9D0EB223}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Extensions.Hosting", "src\OpenFeature.Extensions.Hosting\OpenFeature.Extensions.Hosting.csproj", "{F2DB11D0-15E7-4C1F-936A-37D23EECECC0}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{72005F60-C2E8-40BF-AE95-893635134D7D}"
ProjectSection(SolutionItems) = preProject
.github\workflows\code-coverage.yml = .github\workflows\code-coverage.yml
Expand Down Expand Up @@ -38,6 +40,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.Benchmarks", "t
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.E2ETests", "test\OpenFeature.E2ETests\OpenFeature.E2ETests.csproj", "{90E7EAD3-251E-4490-AF78-E758E33518E5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Extensions.Hosting.Tests", "test\OpenFeature.Extensions.Hosting.Tests\OpenFeature.Extensions.Hosting.Tests.csproj", "{4588FE3C-EB7E-4BA5-BD77-E15131DB3B29}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -48,6 +52,10 @@ Global
{07A6E6BD-FB7E-4B3B-9CBE-65AE9D0EB223}.Debug|Any CPU.Build.0 = Debug|Any CPU
{07A6E6BD-FB7E-4B3B-9CBE-65AE9D0EB223}.Release|Any CPU.ActiveCfg = Release|Any CPU
{07A6E6BD-FB7E-4B3B-9CBE-65AE9D0EB223}.Release|Any CPU.Build.0 = Release|Any CPU
{F2DB11D0-15E7-4C1F-936A-37D23EECECC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F2DB11D0-15E7-4C1F-936A-37D23EECECC0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F2DB11D0-15E7-4C1F-936A-37D23EECECC0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F2DB11D0-15E7-4C1F-936A-37D23EECECC0}.Release|Any CPU.Build.0 = Release|Any CPU
{49BB42BA-10A6-4DA3-A7D5-38C968D57837}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{49BB42BA-10A6-4DA3-A7D5-38C968D57837}.Debug|Any CPU.Build.0 = Debug|Any CPU
{49BB42BA-10A6-4DA3-A7D5-38C968D57837}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand All @@ -56,14 +64,20 @@ Global
{90E7EAD3-251E-4490-AF78-E758E33518E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{90E7EAD3-251E-4490-AF78-E758E33518E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{90E7EAD3-251E-4490-AF78-E758E33518E5}.Release|Any CPU.Build.0 = Release|Any CPU
{4588FE3C-EB7E-4BA5-BD77-E15131DB3B29}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4588FE3C-EB7E-4BA5-BD77-E15131DB3B29}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4588FE3C-EB7E-4BA5-BD77-E15131DB3B29}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4588FE3C-EB7E-4BA5-BD77-E15131DB3B29}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{07A6E6BD-FB7E-4B3B-9CBE-65AE9D0EB223} = {C97E9975-E10A-4817-AE2C-4DD69C3C02D4}
{F2DB11D0-15E7-4C1F-936A-37D23EECECC0} = {C97E9975-E10A-4817-AE2C-4DD69C3C02D4}
{49BB42BA-10A6-4DA3-A7D5-38C968D57837} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9}
{90E7EAD3-251E-4490-AF78-E758E33518E5} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9}
{4588FE3C-EB7E-4BA5-BD77-E15131DB3B29} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {41F01B78-FB06-404F-8AD0-6ED6973F948F}
Expand Down
11 changes: 7 additions & 4 deletions build/Common.prod.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@
<Import Project=".\Common.props" />

<PropertyGroup>
<CI Condition="'$(CI)' == ''">$(DOTNET_RUNNING_IN_CONTAINER)</CI>
<ContinuousIntegrationBuild Condition="'$(CI)' == 'true'">true</ContinuousIntegrationBuild>
<Deterministic Condition="'$(CI)' == 'true'">true</Deterministic>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackRelease>true</PackRelease>
</PropertyGroup>

<PropertyGroup>
Expand All @@ -11,7 +15,6 @@
<RepositoryUrl>https://github.com/open-feature/dotnet-sdk</RepositoryUrl>
<Description>OpenFeature is an open standard for feature flag management, created to support a robust feature flag ecosystem using cloud native technologies. OpenFeature will provide a unified API and SDK, and a developer-first, cloud-native implementation, with extensibility for open source and commercial offerings.</Description>
<PackageTags>Feature;OpenFeature;Flags;</PackageTags>
<PackageId>OpenFeature</PackageId>
<PackageIcon>openfeature-icon.png</PackageIcon>
<PackageProjectUrl>https://openfeature.dev</PackageProjectUrl>
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
Expand All @@ -37,7 +40,7 @@
<SourceRoot Include="$(MSBuildThisFileDirectory)/" />
</ItemGroup>

<PropertyGroup Condition="'$(Deterministic)'=='true'">
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)../src/Shared/**" LinkBase="Shared" />
</ItemGroup>
</Project>
3 changes: 2 additions & 1 deletion build/Common.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<LangVersion>7.3</LangVersion>
<LangVersion>latest</LangVersion>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
</PropertyGroup>
Expand All @@ -19,6 +19,7 @@
Please sort alphabetically.
Refer to https://docs.microsoft.com/nuget/concepts/package-versioning for semver syntax.
-->
<MicrosoftExtensionsHostingAbstractionsVer>[8.0.0,)</MicrosoftExtensionsHostingAbstractionsVer>
<MicrosoftExtensionsLoggerVer>[2.0,)</MicrosoftExtensionsLoggerVer>
<MicrosoftSourceLinkGitHubPkgVer>[1.0.0,2.0)</MicrosoftSourceLinkGitHubPkgVer>
</PropertyGroup>
Expand Down
1 change: 1 addition & 0 deletions build/Common.tests.props
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
<CoverletCollectorVer>[3.1.2]</CoverletCollectorVer>
<FluentAssertionsVer>[6.7.0]</FluentAssertionsVer>
<GitHubActionsTestLoggerVer>[2.3.3]</GitHubActionsTestLoggerVer>
<MicrosoftExtensionsHostingVer>[8.0.0]</MicrosoftExtensionsHostingVer>
<MicrosoftNETTestSdkPkgVer>[17.2.0]</MicrosoftNETTestSdkPkgVer>
<NSubstituteVer>[5.0.0]</NSubstituteVer>
<XUnitRunnerVisualStudioPkgVer>[2.4.3,3.0)</XUnitRunnerVisualStudioPkgVer>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;

namespace OpenFeature.Internal;

/// <summary>
///
/// </summary>
public sealed class OpenFeatureHostedService(Api api, IEnumerable<FeatureProvider> providers) : IHostedLifecycleService
{
readonly Api _api = Check.NotNull(api);
readonly IEnumerable<FeatureProvider> _providers = Check.NotNull(providers);

async Task IHostedLifecycleService.StartingAsync(CancellationToken cancellationToken)
{
foreach (var provider in this._providers)
{
await this._api.SetProvider(provider.GetMetadata().Name, provider).ConfigureAwait(false);

if (this._api.GetProviderMetadata() is { Name: "No-op Provider" })
await this._api.SetProvider(provider).ConfigureAwait(false);
}
}

Task IHostedService.StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;

Task IHostedLifecycleService.StartedAsync(CancellationToken cancellationToken) => Task.CompletedTask;

Task IHostedLifecycleService.StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask;

Task IHostedService.StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;

Task IHostedLifecycleService.StoppedAsync(CancellationToken cancellationToken) => this._api.Shutdown();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<Nullable>enable</Nullable>
<TargetFrameworks>netstandard2.0;net6.0;net7.0;net8.0;net462</TargetFrameworks>
</PropertyGroup>

<PropertyGroup>
<PackageReadmeFile>README.md</PackageReadmeFile>
<RootNamespace>OpenFeature</RootNamespace>
</PropertyGroup>

<ItemGroup>
<None Include="../../README.md" Pack="true" PackagePath="/" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="$(MicrosoftExtensionsHostingAbstractionsVer)" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="../OpenFeature/OpenFeature.csproj" />
</ItemGroup>

</Project>
9 changes: 9 additions & 0 deletions src/OpenFeature.Extensions.Hosting/OpenFeatureBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Microsoft.Extensions.DependencyInjection;

namespace OpenFeature;

/// <summary>
///
/// </summary>
/// <param name="Services"></param>
public sealed record OpenFeatureBuilder(IServiceCollection Services);
145 changes: 145 additions & 0 deletions src/OpenFeature.Extensions.Hosting/OpenFeatureBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using OpenFeature.Internal;
using OpenFeature.Model;

namespace OpenFeature;

/// <summary>
///
/// </summary>
public static class OpenFeatureBuilderExtensions
{
/// <summary>
///
/// </summary>
/// <param name="builder"></param>
/// <param name="configure"></param>
/// <returns>
///
/// </returns>
public static OpenFeatureBuilder AddEvaluationContext(
this OpenFeatureBuilder builder,
Action<EvaluationContextBuilder> configure)
{
Check.NotNull(builder);
Check.NotNull(configure);

AddEvaluationContext(builder, null, (b, _, _) => configure(b));

return builder;
}

/// <summary>
///
/// </summary>
/// <param name="builder"></param>
/// <param name="configure"></param>
/// <returns>
///
/// </returns>
public static OpenFeatureBuilder AddEvaluationContext(
this OpenFeatureBuilder builder,
Action<EvaluationContextBuilder, IServiceProvider> configure)
{
Check.NotNull(builder);
Check.NotNull(configure);

AddEvaluationContext(builder, null, (b, _, s) => configure(b, s));

return builder;
}

/// <summary>
///
/// </summary>
/// <param name="builder"></param>
/// <param name="providerName"></param>
/// <param name="configure"></param>
/// <returns>
///
/// </returns>
public static OpenFeatureBuilder AddEvaluationContext(
this OpenFeatureBuilder builder,
string? providerName,
Action<EvaluationContextBuilder, string?, IServiceProvider> configure)
{
Check.NotNull(builder);
Check.NotNull(configure);

builder.Services.AddKeyedSingleton(providerName, (services, key) =>
{
var b = EvaluationContext.Builder();
configure(b, key as string, services);
return b.Build();
});

return builder;
}

/// <summary>
///
/// </summary>
/// <param name="builder"></param>
/// <param name="providerName"></param>
public static void TryAddOpenFeatureClient(this OpenFeatureBuilder builder, string? providerName = null)
{
Check.NotNull(builder);

builder.Services.AddHostedService<OpenFeatureHostedService>();

builder.Services.TryAddKeyedSingleton(providerName, static (services, providerName) =>
{
var api = providerName switch
{
null => Api.Instance,
not null => services.GetRequiredKeyedService<Api>(null)
};
api.AddHooks(services.GetKeyedServices<Hook>(providerName));
api.SetContext(services.GetRequiredKeyedService<EvaluationContextBuilder>(providerName).Build());
return api;
});

builder.Services.TryAddKeyedSingleton(providerName, static (services, providerName) => providerName switch
{
null => services.GetRequiredService<ILogger<FeatureClient>>(),
not null => services.GetRequiredService<ILoggerFactory>().CreateLogger($"OpenFeature.FeatureClient.{providerName}")
});

builder.Services.TryAddKeyedTransient(providerName, static (services, providerName) =>
{
var builder = providerName switch
{
null => EvaluationContext.Builder(),
not null => services.GetRequiredKeyedService<EvaluationContextBuilder>(null)
};
foreach (var c in services.GetKeyedServices<EvaluationContext>(providerName))
{
builder.Merge(c);
}
return builder;
});

builder.Services.TryAddKeyedTransient<IFeatureClient>(providerName, static (services, providerName) =>
{
var api = services.GetRequiredService<Api>();
return api.GetClient(
api.GetProviderMetadata(providerName as string).Name,
null,
services.GetRequiredKeyedService<ILogger>(providerName),
services.GetRequiredKeyedService<EvaluationContextBuilder>(providerName).Build());
});

if (providerName is not null)
builder.Services.Replace(ServiceDescriptor.Transient(services => services.GetRequiredKeyedService<IFeatureClient>(providerName)));
}
}
Loading

0 comments on commit c295dd0

Please sign in to comment.