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 29, 2024
1 parent 7eebcdd commit bcf19c4
Show file tree
Hide file tree
Showing 13 changed files with 460 additions and 0 deletions.
2 changes: 2 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

<ItemGroup Label="src">
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="2.0.0" />
<PackageVersion Include="System.Collections.Immutable" Version="1.7.1" />
<PackageVersion Include="System.Threading.Channels" Version="6.0.0" />
Expand All @@ -18,6 +19,7 @@
<PackageVersion Include="coverlet.msbuild" Version="3.1.2" />
<PackageVersion Include="FluentAssertions" Version="6.7.0" />
<PackageVersion Include="GitHubActionsTestLogger" Version="2.3.3" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
<PackageVersion Include="NSubstitute" Version="5.0.0" />
<PackageVersion Include="OpenFeature.Contrib.Providers.Flagd" Version="0.1.5" />
Expand Down
14 changes: 14 additions & 0 deletions OpenFeature.sln
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{65FBA159-2
EndProject
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("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.Tests", "test\OpenFeature.Tests\OpenFeature.Tests.csproj", "{49BB42BA-10A6-4DA3-A7D5-38C968D57837}"
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
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.Benchmarks", "test\OpenFeature.Benchmarks\OpenFeature.Benchmarks.csproj", "{90E7EAD3-251E-4490-AF78-E758E33518E5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.E2ETests", "test\OpenFeature.E2ETests\OpenFeature.E2ETests.csproj", "{7398C446-2630-4F8C-9278-4E807720E9E5}"
Expand All @@ -89,10 +93,18 @@ 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
{49BB42BA-10A6-4DA3-A7D5-38C968D57837}.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
{90E7EAD3-251E-4490-AF78-E758E33518E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{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
Expand All @@ -107,7 +119,9 @@ Global
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}
{4588FE3C-EB7E-4BA5-BD77-E15131DB3B29} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9}
{90E7EAD3-251E-4490-AF78-E758E33518E5} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9}
{7398C446-2630-4F8C-9278-4E807720E9E5} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9}
{C4746B8C-FE19-440B-922C-C2377F906FE8} = {2B172AA0-A5A6-4D94-BA1F-B79D59B0C2D8}
Expand Down
3 changes: 3 additions & 0 deletions build/Common.prod.props
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,7 @@
<None Include="$(MSBuildThisFileDirectory)openfeature-icon.png" Pack="true" PackagePath="\" />
</ItemGroup>

<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)../src/Shared/**" LinkBase="Shared" />
</ItemGroup>
</Project>
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.SetProviderAsync(provider.GetMetadata().Name, provider).ConfigureAwait(false);

if (this._api.GetProviderMetadata() is { Name: "No-op Provider" })
await this._api.SetProviderAsync(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" />
</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 AddContext(
this OpenFeatureBuilder builder,
Action<EvaluationContextBuilder> configure)
{
Check.NotNull(builder);
Check.NotNull(configure);

Check warning on line 28 in src/OpenFeature.Extensions.Hosting/OpenFeatureBuilderExtensions.cs

View check run for this annotation

Codecov / codecov/patch

src/OpenFeature.Extensions.Hosting/OpenFeatureBuilderExtensions.cs#L26-L28

Added lines #L26 - L28 were not covered by tests

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

Check warning on line 30 in src/OpenFeature.Extensions.Hosting/OpenFeatureBuilderExtensions.cs

View check run for this annotation

Codecov / codecov/patch

src/OpenFeature.Extensions.Hosting/OpenFeatureBuilderExtensions.cs#L30

Added line #L30 was not covered by tests

return builder;
}

Check warning on line 33 in src/OpenFeature.Extensions.Hosting/OpenFeatureBuilderExtensions.cs

View check run for this annotation

Codecov / codecov/patch

src/OpenFeature.Extensions.Hosting/OpenFeatureBuilderExtensions.cs#L32-L33

Added lines #L32 - L33 were not covered by tests

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

Check warning on line 48 in src/OpenFeature.Extensions.Hosting/OpenFeatureBuilderExtensions.cs

View check run for this annotation

Codecov / codecov/patch

src/OpenFeature.Extensions.Hosting/OpenFeatureBuilderExtensions.cs#L46-L48

Added lines #L46 - L48 were not covered by tests

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

Check warning on line 50 in src/OpenFeature.Extensions.Hosting/OpenFeatureBuilderExtensions.cs

View check run for this annotation

Codecov / codecov/patch

src/OpenFeature.Extensions.Hosting/OpenFeatureBuilderExtensions.cs#L50

Added line #L50 was not covered by tests

return builder;
}

Check warning on line 53 in src/OpenFeature.Extensions.Hosting/OpenFeatureBuilderExtensions.cs

View check run for this annotation

Codecov / codecov/patch

src/OpenFeature.Extensions.Hosting/OpenFeatureBuilderExtensions.cs#L52-L53

Added lines #L52 - L53 were not covered by tests

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

Check warning on line 70 in src/OpenFeature.Extensions.Hosting/OpenFeatureBuilderExtensions.cs

View check run for this annotation

Codecov / codecov/patch

src/OpenFeature.Extensions.Hosting/OpenFeatureBuilderExtensions.cs#L68-L70

Added lines #L68 - L70 were not covered by tests

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

Check warning on line 79 in src/OpenFeature.Extensions.Hosting/OpenFeatureBuilderExtensions.cs

View check run for this annotation

Codecov / codecov/patch

src/OpenFeature.Extensions.Hosting/OpenFeatureBuilderExtensions.cs#L72-L79

Added lines #L72 - L79 were not covered by tests

return builder;
}

Check warning on line 82 in src/OpenFeature.Extensions.Hosting/OpenFeatureBuilderExtensions.cs

View check run for this annotation

Codecov / codecov/patch

src/OpenFeature.Extensions.Hosting/OpenFeatureBuilderExtensions.cs#L81-L82

Added lines #L81 - L82 were not covered by tests

/// <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)

Check warning on line 100 in src/OpenFeature.Extensions.Hosting/OpenFeatureBuilderExtensions.cs

View check run for this annotation

Codecov / codecov/patch

src/OpenFeature.Extensions.Hosting/OpenFeatureBuilderExtensions.cs#L100

Added line #L100 was not covered by tests
};
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);
}

Check warning on line 126 in src/OpenFeature.Extensions.Hosting/OpenFeatureBuilderExtensions.cs

View check run for this annotation

Codecov / codecov/patch

src/OpenFeature.Extensions.Hosting/OpenFeatureBuilderExtensions.cs#L124-L126

Added lines #L124 - L126 were not covered by tests
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;
}

Check warning on line 19 in src/Shared/CallerArgumentExpressionAttribute.cs

View check run for this annotation

Codecov / codecov/patch

src/Shared/CallerArgumentExpressionAttribute.cs#L16-L19

Added lines #L16 - L19 were not covered by tests

public string ParameterName { get; }

Check warning on line 21 in src/Shared/CallerArgumentExpressionAttribute.cs

View check run for this annotation

Codecov / codecov/patch

src/Shared/CallerArgumentExpressionAttribute.cs#L21

Added line #L21 was not covered by tests
}
}
#endif
22 changes: 22 additions & 0 deletions src/Shared/Check.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#nullable enable
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;

namespace OpenFeature;

[DebuggerStepThrough]
static class Check
{
public static T NotNull<T>(T? value, [CallerArgumentExpression("value")] string name = null!)
{
#if NET8_0_OR_GREATER
ArgumentNullException.ThrowIfNull(value, name);
#else
if (value is null)
throw new ArgumentNullException(name);

Check warning on line 17 in src/Shared/Check.cs

View check run for this annotation

Codecov / codecov/patch

src/Shared/Check.cs#L17

Added line #L17 was not covered by tests
#endif

return value;
}
}
Loading

0 comments on commit bcf19c4

Please sign in to comment.