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 <austin@austindrenski.io>
  • Loading branch information
austindrenski committed Jan 16, 2024
1 parent 37f7d10 commit b5fa35e
Show file tree
Hide file tree
Showing 15 changed files with 460 additions and 3 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
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
4 changes: 4 additions & 0 deletions build/Common.prod.props
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,8 @@
<PropertyGroup Condition="'$(Deterministic)'=='true'">
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>

<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)../src/Shared/**" LinkBase="Shared" />
</ItemGroup>
</Project>
4 changes: 3 additions & 1 deletion build/Common.props
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
<Project>
<PropertyGroup>
<LangVersion>7.3</LangVersion>
<LangVersion>latest</LangVersion>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<Nullable>enable</Nullable>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)'=='Debug'">
Expand All @@ -19,6 +20,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,21 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0;net7.0;net8.0;net462</TargetFrameworks>
<RootNamespace>OpenFeature</RootNamespace>
<PackageReadmeFile>README.md</PackageReadmeFile>
</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<ILogger>(providerName, static (services, providerName) => providerName switch
{
null => services.GetRequiredService<ILogger<FeatureClient>>(),
not null => services.GetRequiredService<ILoggerFactory>().CreateLogger($"OpenFeature.FeatureClient.{providerName}")
});

builder.Services.TryAddKeyedTransient<EvaluationContextBuilder>(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)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using System;
using OpenFeature;

#pragma warning disable IDE0130 // Namespace does not match folder structure
// ReSharper disable once CheckNamespace
namespace Microsoft.Extensions.DependencyInjection;

/// <summary>
///
/// </summary>
public static class OpenFeatureServiceCollectionExtensions
{
/// <summary>
///
/// </summary>
/// <param name="services"></param>
/// <param name="configure"></param>
/// <returns></returns>
public static IServiceCollection AddOpenFeature(this IServiceCollection services, Action<OpenFeatureBuilder> configure)
{
Check.NotNull(services);
Check.NotNull(configure);

configure(AddOpenFeature(services));

return services;
}

/// <summary>
///
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
public static OpenFeatureBuilder AddOpenFeature(this IServiceCollection services)
{
Check.NotNull(services);

var builder = new OpenFeatureBuilder(services);

builder.TryAddOpenFeatureClient();

return builder;
}
}
24 changes: 24 additions & 0 deletions src/Shared/CallerArgumentExpressionAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// @formatter:off
// ReSharper disable All
#if NETCOREAPP3_0_OR_GREATER
// https://github.com/dotnet/runtime/issues/96197
[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Runtime.CompilerServices.CallerArgumentExpressionAttribute))]
#else
#pragma warning disable
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
internal sealed class CallerArgumentExpressionAttribute : Attribute
{
public CallerArgumentExpressionAttribute(string parameterName)
{
ParameterName = parameterName;
}

public string ParameterName { get; }
}
}
#endif
Loading

0 comments on commit b5fa35e

Please sign in to comment.