Skip to content

Commit aa8544b

Browse files
committed
feat: Add OpenFeature.Extensions.Hosting package
See: open-feature/dotnet-sdk-contrib#127 Signed-off-by: Austin Drenski <[email protected]>
1 parent b7b9ad7 commit aa8544b

16 files changed

+468
-15
lines changed

.github/workflows/ci.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626
7.0.x
2727
2828
- name: Run Tests
29-
run: dotnet test test/OpenFeature.Tests/ --configuration Release --logger GitHubActions
29+
run: dotnet test --logger GitHubActions
3030

3131
unit-tests-windows:
3232
runs-on: windows-latest
@@ -43,4 +43,4 @@ jobs:
4343
7.0.x
4444
4545
- name: Run Tests
46-
run: dotnet test test\OpenFeature.Tests\ --configuration Release --logger GitHubActions
46+
run: dotnet test --logger GitHubActions

.github/workflows/release.yml

+2-8
Original file line numberDiff line numberDiff line change
@@ -34,19 +34,13 @@ jobs:
3434
if: ${{ steps.release.outputs.releases_created }}
3535
run: dotnet restore
3636

37-
- name: Build
38-
if: ${{ steps.release.outputs.releases_created }}
39-
run: |
40-
dotnet build --configuration Release --no-restore -p:Deterministic=true
41-
4237
- name: Pack
4338
if: ${{ steps.release.outputs.releases_created }}
44-
run: |
45-
dotnet pack OpenFeature.proj --configuration Release --no-build -p:PackageID=OpenFeature
39+
run: dotnet pack --no-restore
4640

4741
- name: Publish to Nuget
4842
if: ${{ steps.release.outputs.releases_created }}
4943
run: |
50-
dotnet nuget push src/OpenFeature/bin/Release/OpenFeature.${{ steps.release.outputs.major }}.${{ steps.release.outputs.minor }}.${{ steps.release.outputs.patch }}.nupkg `
44+
dotnet nuget push "src/**/*.nupkg" `
5145
--api-key ${{secrets.NUGET_TOKEN}} `
5246
--source https://api.nuget.org/v3/index.json

OpenFeature.sln

+14
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ VisualStudioVersion = 17.4.33213.308
55
MinimumVisualStudioVersion = 10.0.40219.1
66
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature", "src\OpenFeature\OpenFeature.csproj", "{07A6E6BD-FB7E-4B3B-9CBE-65AE9D0EB223}"
77
EndProject
8+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Extensions.Hosting", "src\OpenFeature.Extensions.Hosting\OpenFeature.Extensions.Hosting.csproj", "{F2DB11D0-15E7-4C1F-936A-37D23EECECC0}"
9+
EndProject
810
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{72005F60-C2E8-40BF-AE95-893635134D7D}"
911
ProjectSection(SolutionItems) = preProject
1012
.github\workflows\code-coverage.yml = .github\workflows\code-coverage.yml
@@ -38,6 +40,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.Benchmarks", "t
3840
EndProject
3941
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.E2ETests", "test\OpenFeature.E2ETests\OpenFeature.E2ETests.csproj", "{90E7EAD3-251E-4490-AF78-E758E33518E5}"
4042
EndProject
43+
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}"
44+
EndProject
4145
Global
4246
GlobalSection(SolutionConfigurationPlatforms) = preSolution
4347
Debug|Any CPU = Debug|Any CPU
@@ -48,6 +52,10 @@ Global
4852
{07A6E6BD-FB7E-4B3B-9CBE-65AE9D0EB223}.Debug|Any CPU.Build.0 = Debug|Any CPU
4953
{07A6E6BD-FB7E-4B3B-9CBE-65AE9D0EB223}.Release|Any CPU.ActiveCfg = Release|Any CPU
5054
{07A6E6BD-FB7E-4B3B-9CBE-65AE9D0EB223}.Release|Any CPU.Build.0 = Release|Any CPU
55+
{F2DB11D0-15E7-4C1F-936A-37D23EECECC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
56+
{F2DB11D0-15E7-4C1F-936A-37D23EECECC0}.Debug|Any CPU.Build.0 = Debug|Any CPU
57+
{F2DB11D0-15E7-4C1F-936A-37D23EECECC0}.Release|Any CPU.ActiveCfg = Release|Any CPU
58+
{F2DB11D0-15E7-4C1F-936A-37D23EECECC0}.Release|Any CPU.Build.0 = Release|Any CPU
5159
{49BB42BA-10A6-4DA3-A7D5-38C968D57837}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
5260
{49BB42BA-10A6-4DA3-A7D5-38C968D57837}.Debug|Any CPU.Build.0 = Debug|Any CPU
5361
{49BB42BA-10A6-4DA3-A7D5-38C968D57837}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -56,14 +64,20 @@ Global
5664
{90E7EAD3-251E-4490-AF78-E758E33518E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
5765
{90E7EAD3-251E-4490-AF78-E758E33518E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
5866
{90E7EAD3-251E-4490-AF78-E758E33518E5}.Release|Any CPU.Build.0 = Release|Any CPU
67+
{4588FE3C-EB7E-4BA5-BD77-E15131DB3B29}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
68+
{4588FE3C-EB7E-4BA5-BD77-E15131DB3B29}.Debug|Any CPU.Build.0 = Debug|Any CPU
69+
{4588FE3C-EB7E-4BA5-BD77-E15131DB3B29}.Release|Any CPU.ActiveCfg = Release|Any CPU
70+
{4588FE3C-EB7E-4BA5-BD77-E15131DB3B29}.Release|Any CPU.Build.0 = Release|Any CPU
5971
EndGlobalSection
6072
GlobalSection(SolutionProperties) = preSolution
6173
HideSolutionNode = FALSE
6274
EndGlobalSection
6375
GlobalSection(NestedProjects) = preSolution
6476
{07A6E6BD-FB7E-4B3B-9CBE-65AE9D0EB223} = {C97E9975-E10A-4817-AE2C-4DD69C3C02D4}
77+
{F2DB11D0-15E7-4C1F-936A-37D23EECECC0} = {C97E9975-E10A-4817-AE2C-4DD69C3C02D4}
6578
{49BB42BA-10A6-4DA3-A7D5-38C968D57837} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9}
6679
{90E7EAD3-251E-4490-AF78-E758E33518E5} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9}
80+
{4588FE3C-EB7E-4BA5-BD77-E15131DB3B29} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9}
6781
EndGlobalSection
6882
GlobalSection(ExtensibilityGlobals) = postSolution
6983
SolutionGuid = {41F01B78-FB06-404F-8AD0-6ED6973F948F}

build/Common.prod.props

+7-4
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22
<Import Project=".\Common.props" />
33

44
<PropertyGroup>
5+
<CI Condition="'$(CI)' == ''">$(DOTNET_RUNNING_IN_CONTAINER)</CI>
6+
<ContinuousIntegrationBuild Condition="'$(CI)' == 'true'">true</ContinuousIntegrationBuild>
7+
<Deterministic Condition="'$(CI)' == 'true'">true</Deterministic>
58
<GenerateDocumentationFile>true</GenerateDocumentationFile>
9+
<PackRelease>true</PackRelease>
610
</PropertyGroup>
711

812
<PropertyGroup>
@@ -11,7 +15,6 @@
1115
<RepositoryUrl>https://github.com/open-feature/dotnet-sdk</RepositoryUrl>
1216
<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>
1317
<PackageTags>Feature;OpenFeature;Flags;</PackageTags>
14-
<PackageId>OpenFeature</PackageId>
1518
<PackageIcon>openfeature-icon.png</PackageIcon>
1619
<PackageProjectUrl>https://openfeature.dev</PackageProjectUrl>
1720
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
@@ -37,7 +40,7 @@
3740
<SourceRoot Include="$(MSBuildThisFileDirectory)/" />
3841
</ItemGroup>
3942

40-
<PropertyGroup Condition="'$(Deterministic)'=='true'">
41-
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
42-
</PropertyGroup>
43+
<ItemGroup>
44+
<Compile Include="$(MSBuildThisFileDirectory)../src/Shared/**" LinkBase="Shared" />
45+
</ItemGroup>
4346
</Project>

build/Common.props

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<Project>
22
<PropertyGroup>
3-
<LangVersion>7.3</LangVersion>
3+
<LangVersion>latest</LangVersion>
44
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
55
<EnableNETAnalyzers>true</EnableNETAnalyzers>
66
</PropertyGroup>
@@ -20,6 +20,7 @@
2020
Refer to https://docs.microsoft.com/nuget/concepts/package-versioning for semver syntax.
2121
-->
2222
<MicrosoftBclAsyncInterfacesVer>[8.0.0,)</MicrosoftBclAsyncInterfacesVer>
23+
<MicrosoftExtensionsHostingAbstractionsVer>[8.0.0,)</MicrosoftExtensionsHostingAbstractionsVer>
2324
<MicrosoftExtensionsLoggerVer>[2.0,)</MicrosoftExtensionsLoggerVer>
2425
<MicrosoftSourceLinkGitHubPkgVer>[1.0.0,2.0)</MicrosoftSourceLinkGitHubPkgVer>
2526
</PropertyGroup>

build/Common.tests.props

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
<CoverletCollectorVer>[3.1.2]</CoverletCollectorVer>
3030
<FluentAssertionsVer>[6.7.0]</FluentAssertionsVer>
3131
<GitHubActionsTestLoggerVer>[2.3.3]</GitHubActionsTestLoggerVer>
32+
<MicrosoftExtensionsHostingVer>[8.0.0]</MicrosoftExtensionsHostingVer>
3233
<MicrosoftNETTestSdkPkgVer>[17.2.0]</MicrosoftNETTestSdkPkgVer>
3334
<NSubstituteVer>[5.0.0]</NSubstituteVer>
3435
<XUnitRunnerVisualStudioPkgVer>[2.4.3,3.0)</XUnitRunnerVisualStudioPkgVer>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using System.Collections.Generic;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
using Microsoft.Extensions.Hosting;
5+
6+
namespace OpenFeature.Internal;
7+
8+
/// <summary>
9+
///
10+
/// </summary>
11+
public sealed class OpenFeatureHostedService(Api api, IEnumerable<FeatureProvider> providers) : IHostedLifecycleService
12+
{
13+
readonly Api _api = Check.NotNull(api);
14+
readonly IEnumerable<FeatureProvider> _providers = Check.NotNull(providers);
15+
16+
async Task IHostedLifecycleService.StartingAsync(CancellationToken cancellationToken)
17+
{
18+
foreach (var provider in this._providers)
19+
{
20+
await this._api.SetProvider(provider.GetMetadata().Name, provider).ConfigureAwait(false);
21+
22+
if (this._api.GetProviderMetadata() is { Name: "No-op Provider" })
23+
await this._api.SetProvider(provider).ConfigureAwait(false);
24+
}
25+
}
26+
27+
Task IHostedService.StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
28+
29+
Task IHostedLifecycleService.StartedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
30+
31+
Task IHostedLifecycleService.StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
32+
33+
Task IHostedService.StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
34+
35+
Task IHostedLifecycleService.StoppedAsync(CancellationToken cancellationToken) => this._api.Shutdown();
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<Nullable>enable</Nullable>
5+
<TargetFrameworks>netstandard2.0;net6.0;net7.0;net8.0;net462</TargetFrameworks>
6+
</PropertyGroup>
7+
8+
<PropertyGroup>
9+
<PackageReadmeFile>README.md</PackageReadmeFile>
10+
<RootNamespace>OpenFeature</RootNamespace>
11+
</PropertyGroup>
12+
13+
<ItemGroup>
14+
<None Include="../../README.md" Pack="true" PackagePath="/" />
15+
</ItemGroup>
16+
17+
<ItemGroup>
18+
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="$(MicrosoftExtensionsHostingAbstractionsVer)" />
19+
</ItemGroup>
20+
21+
<ItemGroup>
22+
<ProjectReference Include="../OpenFeature/OpenFeature.csproj" />
23+
</ItemGroup>
24+
25+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
3+
namespace OpenFeature;
4+
5+
/// <summary>
6+
///
7+
/// </summary>
8+
/// <param name="Services"></param>
9+
public sealed record OpenFeatureBuilder(IServiceCollection Services);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
using System;
2+
using Microsoft.Extensions.DependencyInjection;
3+
using Microsoft.Extensions.DependencyInjection.Extensions;
4+
using Microsoft.Extensions.Logging;
5+
using OpenFeature.Internal;
6+
using OpenFeature.Model;
7+
8+
namespace OpenFeature;
9+
10+
/// <summary>
11+
///
12+
/// </summary>
13+
public static class OpenFeatureBuilderExtensions
14+
{
15+
/// <summary>
16+
///
17+
/// </summary>
18+
/// <param name="builder"></param>
19+
/// <param name="configure"></param>
20+
/// <returns>
21+
///
22+
/// </returns>
23+
public static OpenFeatureBuilder AddEvaluationContext(
24+
this OpenFeatureBuilder builder,
25+
Action<EvaluationContextBuilder> configure)
26+
{
27+
Check.NotNull(builder);
28+
Check.NotNull(configure);
29+
30+
AddEvaluationContext(builder, null, (b, _, _) => configure(b));
31+
32+
return builder;
33+
}
34+
35+
/// <summary>
36+
///
37+
/// </summary>
38+
/// <param name="builder"></param>
39+
/// <param name="configure"></param>
40+
/// <returns>
41+
///
42+
/// </returns>
43+
public static OpenFeatureBuilder AddEvaluationContext(
44+
this OpenFeatureBuilder builder,
45+
Action<EvaluationContextBuilder, IServiceProvider> configure)
46+
{
47+
Check.NotNull(builder);
48+
Check.NotNull(configure);
49+
50+
AddEvaluationContext(builder, null, (b, _, s) => configure(b, s));
51+
52+
return builder;
53+
}
54+
55+
/// <summary>
56+
///
57+
/// </summary>
58+
/// <param name="builder"></param>
59+
/// <param name="providerName"></param>
60+
/// <param name="configure"></param>
61+
/// <returns>
62+
///
63+
/// </returns>
64+
public static OpenFeatureBuilder AddEvaluationContext(
65+
this OpenFeatureBuilder builder,
66+
string? providerName,
67+
Action<EvaluationContextBuilder, string?, IServiceProvider> configure)
68+
{
69+
Check.NotNull(builder);
70+
Check.NotNull(configure);
71+
72+
builder.Services.AddKeyedSingleton(providerName, (services, key) =>
73+
{
74+
var b = EvaluationContext.Builder();
75+
76+
configure(b, key as string, services);
77+
78+
return b.Build();
79+
});
80+
81+
return builder;
82+
}
83+
84+
/// <summary>
85+
///
86+
/// </summary>
87+
/// <param name="builder"></param>
88+
/// <param name="providerName"></param>
89+
public static void TryAddOpenFeatureClient(this OpenFeatureBuilder builder, string? providerName = null)
90+
{
91+
Check.NotNull(builder);
92+
93+
builder.Services.AddHostedService<OpenFeatureHostedService>();
94+
95+
builder.Services.TryAddKeyedSingleton(providerName, static (services, providerName) =>
96+
{
97+
var api = providerName switch
98+
{
99+
null => Api.Instance,
100+
not null => services.GetRequiredKeyedService<Api>(null)
101+
};
102+
103+
api.AddHooks(services.GetKeyedServices<Hook>(providerName));
104+
api.SetContext(services.GetRequiredKeyedService<EvaluationContextBuilder>(providerName).Build());
105+
106+
return api;
107+
});
108+
109+
builder.Services.TryAddKeyedSingleton(providerName, static (services, providerName) => providerName switch
110+
{
111+
null => services.GetRequiredService<ILogger<FeatureClient>>(),
112+
not null => services.GetRequiredService<ILoggerFactory>().CreateLogger($"OpenFeature.FeatureClient.{providerName}")
113+
});
114+
115+
builder.Services.TryAddKeyedTransient(providerName, static (services, providerName) =>
116+
{
117+
var builder = providerName switch
118+
{
119+
null => EvaluationContext.Builder(),
120+
not null => services.GetRequiredKeyedService<EvaluationContextBuilder>(null)
121+
};
122+
123+
foreach (var c in services.GetKeyedServices<EvaluationContext>(providerName))
124+
{
125+
builder.Merge(c);
126+
}
127+
128+
return builder;
129+
});
130+
131+
builder.Services.TryAddKeyedTransient<IFeatureClient>(providerName, static (services, providerName) =>
132+
{
133+
var api = services.GetRequiredService<Api>();
134+
135+
return api.GetClient(
136+
api.GetProviderMetadata(providerName as string).Name,
137+
null,
138+
services.GetRequiredKeyedService<ILogger>(providerName),
139+
services.GetRequiredKeyedService<EvaluationContextBuilder>(providerName).Build());
140+
});
141+
142+
if (providerName is not null)
143+
builder.Services.Replace(ServiceDescriptor.Transient(services => services.GetRequiredKeyedService<IFeatureClient>(providerName)));
144+
}
145+
}

0 commit comments

Comments
 (0)