Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
b6dadb7
Add flagd hosting integration with configuration and tests
askpt Sep 1, 2025
1e66a99
Update Flagd container image tag to v0.12.9
askpt Sep 1, 2025
9215639
Moved Fladg example project and configuration files
askpt Sep 1, 2025
9497a25
Refactor Flagd resource configuration and add health check support
askpt Sep 1, 2025
9fb58c1
Refactor AddFlagd method calls to use named parameters for clarity
askpt Sep 1, 2025
51b1fa3
Update AddFlagd method to use a default port value instead of nullable
askpt Sep 1, 2025
8c2a797
Update README.md to clarify usage and configuration for AddFlagd method
askpt Sep 1, 2025
1d429a1
Enhance AddFlagd method to validate port range and update container i…
askpt Sep 1, 2025
6dd7886
Add examples folder to solution structure
askpt Sep 1, 2025
477b38f
Add OpenFeature Flagd provider and configuration flags
askpt Sep 2, 2025
d552fc8
Update AddFlagd method documentation to specify the expected flag con…
askpt Sep 2, 2025
c604625
Fix AddFlagd method to correctly set target port for HTTP endpoint
askpt Sep 2, 2025
ec99e79
Update README to correct flag configuration file path and enhance usa…
askpt Sep 2, 2025
6432c91
Refactor AddFlagd method to accept optional port parameter and update…
askpt Sep 4, 2025
1a34af8
Refactor AddFlagd method to use constant for target port in HTTP endp…
askpt Sep 4, 2025
a597e40
Refactor logging configuration in AddFlagd method to use default logg…
askpt Sep 4, 2025
ef086c5
Add tests for flagd provider and assert expected values
askpt Sep 15, 2025
1b04555
Add health check endpoint to Flagd resource and builder
askpt Sep 21, 2025
a40d022
Add OFREP endpoint support to Flagd resource and builder
askpt Oct 1, 2025
7cb777f
Refactor AddFlagd method to use WithBindFileSync for flag configuration
askpt Oct 1, 2025
8ebce6b
Fix documentation for AddFlagd method to include remarks about sync s…
askpt Oct 1, 2025
ba0424d
Update AddFlagdTests and AppHostTests to improve resource handling an…
askpt Oct 1, 2025
35c60ff
Add support for OpenFeature.Providers.Ofrep and update related config…
askpt Oct 1, 2025
1e550c2
Fix Ofrep evaluation test by updating connection string retrieval and…
askpt Oct 1, 2025
b043e89
Update README to reflect changes in AddFlagd usage with WithBindFileS…
askpt Oct 2, 2025
2cb3e67
Apply suggestion from @Copilot
askpt Oct 3, 2025
60f78ef
Clarify logging and port customization in README
askpt Oct 3, 2025
518c086
Add Hosting.Flagd.Tests to integration test matrix
askpt Oct 13, 2025
8ea83ca
Add WithLoglevel method to configure logging level for flagd
askpt Oct 13, 2025
6e646a2
Add tests for WithLoglevel method to validate environment variable ad…
askpt Oct 13, 2025
a794065
Refactor logging configuration: replace WithLogging method with WithL…
askpt Oct 13, 2025
310e58e
Merge branch 'main' into askpt/737-add-flagd-hosting
aaronpowell Oct 16, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ jobs:
Hosting.DbGate.Tests,
Hosting.Deno.Tests,
Hosting.EventStore.Tests,
Hosting.Flagd.Tests,
Hosting.GoFeatureFlag.Tests,
Hosting.Golang.Tests,
Hosting.Java.Tests,
Expand Down
5 changes: 5 additions & 0 deletions CommunityToolkit.Aspire.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@
<Project Path="examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.AppHost/CommunityToolkit.Aspire.Hosting.EventStore.AppHost.csproj" />
<Project Path="examples/eventstore/CommunityToolkit.Aspire.Hosting.EventStore.ServiceDefaults/CommunityToolkit.Aspire.Hosting.EventStore.ServiceDefaults.csproj" />
</Folder>
<Folder Name="/examples/flagd/">
<Project Path="examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/CommunityToolkit.Aspire.Hosting.Flagd.AppHost.csproj" />
</Folder>
<Folder Name="/examples/goff/">
<Project Path="examples/goff/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.ApiService/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.ApiService.csproj" />
<Project Path="examples/goff/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.AppHost/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.AppHost.csproj" />
Expand Down Expand Up @@ -166,6 +169,7 @@
<Project Path="src/CommunityToolkit.Aspire.Hosting.DbGate/CommunityToolkit.Aspire.Hosting.DbGate.csproj" />
<Project Path="src/CommunityToolkit.Aspire.Hosting.Deno/CommunityToolkit.Aspire.Hosting.Deno.csproj" />
<Project Path="src/CommunityToolkit.Aspire.Hosting.EventStore/CommunityToolkit.Aspire.Hosting.EventStore.csproj" />
<Project Path="src/CommunityToolkit.Aspire.Hosting.Flagd/CommunityToolkit.Aspire.Hosting.Flagd.csproj" />
<Project Path="src/CommunityToolkit.Aspire.Hosting.GoFeatureFlag/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.csproj" />
<Project Path="src/CommunityToolkit.Aspire.Hosting.Golang/CommunityToolkit.Aspire.Hosting.Golang.csproj" />
<Project Path="src/CommunityToolkit.Aspire.Hosting.Java/CommunityToolkit.Aspire.Hosting.Java.csproj" />
Expand Down Expand Up @@ -217,6 +221,7 @@
<Project Path="tests/CommunityToolkit.Aspire.Hosting.DbGate.Tests/CommunityToolkit.Aspire.Hosting.DbGate.Tests.csproj" />
<Project Path="tests/CommunityToolkit.Aspire.Hosting.Deno.Tests/CommunityToolkit.Aspire.Hosting.Deno.Tests.csproj" />
<Project Path="tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests/CommunityToolkit.Aspire.Hosting.EventStore.Tests.csproj" />
<Project Path="tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests.csproj" />
<Project Path="tests/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.Tests/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.Tests.csproj" />
<Project Path="tests/CommunityToolkit.Aspire.Hosting.Golang.Tests/CommunityToolkit.Aspire.Hosting.Golang.Tests.csproj" />
<Project Path="tests/CommunityToolkit.Aspire.Hosting.Java.Tests/CommunityToolkit.Aspire.Hosting.Java.Tests.csproj" />
Expand Down
2 changes: 2 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@
<PackageVersion Include="Aspire.Hosting.Testing" Version="$(AspireVersion)" />
<PackageVersion Include="Bogus" Version="35.6.3" />
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
<PackageVersion Include="OpenFeature.Contrib.Providers.Flagd" Version="0.3.3" />
<PackageVersion Include="OpenFeature.Providers.Ofrep" Version="0.1.2" />
<PackageVersion Include="MartinCostello.Logging.XUnit" Version="0.6.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageVersion Include="xunit" Version="2.9.3" />
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ This repository contains the source code for the .NET Aspire Community Toolkit,
| - **Learn More**: [`Hosting.Python.Extensions`][python-ext-integration-docs] <br /> - Stable 📦: [![CommunityToolkit.Aspire.Python.Extensions][python-ext-shields]][python-ext-nuget] <br /> - Preview 📦: [![CommunityToolkit.Aspire.Hosting.Python.Extensions][python-ext-shields-preview]][python-ext-nuget-preview] | An integration that contains some additional extensions for running python applications |
| - **Learn More**: [`Hosting.EventStore`][eventstore-integration-docs] <br /> - Stable 📦: [![CommunityToolkit.Aspire.Hosting.EventStore][eventstore-shields]][eventstore-nuget] <br /> - Preview 📦: [![CommunityToolkit.Aspire.Hosting.EventStore][eventstore-shields-preview]][eventstore-nuget-preview] | An Aspire hosting integration leveraging the [EventStore](https://eventstore.com) container. |
| - **Learn More**: [`EventStore`][eventstore-integration-docs] <br /> - Stable 📦: [![CommunityToolkit.Aspire.EventStore][eventstore-client-shields]][eventstore-client-nuget] <br /> - Preview 📦: [![CommunityToolkit.Aspire.EventStore][eventstore-client-shields-preview]][eventstore-client-nuget-preview] | An Aspire client integration for the [EventStore](https://github.com/EventStore/EventStore-Client-Dotnet) package. |
| - **Learn More**: [`Hosting.Flagd`][flagd-integration-docs] <br /> - Stable 📦: [![CommunityToolkit.Aspire.Hosting.Flagd][flagd-shields]][flagd-nuget] <br /> - Preview 📦: [![CommunityToolkit.Aspire.Hosting.Flagd][flagd-shields-preview]][flagd-nuget-preview] | A .NET Aspire hosting integration for [flagd](https://flagd.dev), a feature flag evaluation engine. |
| - **Learn More**: [`Hosting.ActiveMQ`][activemq-integration-docs] <br /> - Stable 📦: [![CommunityToolkit.Aspire.Hosting.ActiveMQ][activemq-shields]][activemq-nuget] <br /> - Preview 📦: [![CommunityToolkit.Aspire.Hosting.ActiveMQ][activemq-shields-preview]][activemq-nuget-preview] | An Aspire hosting integration leveraging the [ActiveMq](https://activemq.apache.org) container. |
| - **Learn More**: [`Hosting.Sqlite`][sqlite-integration-docs] <br /> - Stable 📦: [![CommunityToolkit.Aspire.Hosting.Sqlite][sqlite-shields]][sqlite-hosting-nuget] <br /> - Preview 📦: [![CommunityToolkit.Aspire.Hosting.Sqlite][sqlite-shields-preview]][sqlite-hosting-nuget-preview] | An Aspire hosting integration to setup a SQLite database with optional SQLite Web as a dev UI. |
| - **Learn More**: [`Microsoft.Data.Sqlite`][sqlite-integration-docs] <br /> - Stable 📦: [![CommunityToolkit.Aspire.Microsoft.Data.Sqlite][sqlite-shields]][sqlite-nuget] <br /> - Preview 📦: [![CommunityToolkit.Aspire.Microsoft.Data.Sqlite][sqlite-shields-preview]][sqlite-nuget-preview] | An Aspire client integration for the Microsoft.Data.Sqlite NuGet package. |
Expand Down Expand Up @@ -163,6 +164,11 @@ This project is supported by the [.NET Foundation](https://dotnetfoundation.org)
[eventstore-client-nuget]: https://nuget.org/packages/CommunityToolkit.Aspire.EventStore/
[eventstore-client-shields-preview]: https://img.shields.io/nuget/vpre/CommunityToolkit.Aspire.EventStore?label=nuget%20(preview)
[eventstore-client-nuget-preview]: https://nuget.org/packages/CommunityToolkit.Aspire.EventStore/absoluteLatest
[flagd-integration-docs]: https://learn.microsoft.com/dotnet/aspire/community-toolkit/hosting-flagd
[flagd-shields]: https://img.shields.io/nuget/v/CommunityToolkit.Aspire.Hosting.Flagd
[flagd-nuget]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.Flagd/
[flagd-shields-preview]: https://img.shields.io/nuget/vpre/CommunityToolkit.Aspire.Hosting.Flagd?label=nuget%20(preview)
[flagd-nuget-preview]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.Flagd/absoluteLatest
[activemq-integration-docs]: https://learn.microsoft.com/dotnet/aspire/community-toolkit/hosting-activemq
[activemq-shields]: https://img.shields.io/nuget/v/CommunityToolkit.Aspire.Hosting.ActiveMQ
[activemq-nuget]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.ActiveMQ/
Expand Down Expand Up @@ -268,3 +274,4 @@ This project is supported by the [.NET Foundation](https://dotnetfoundation.org)
[surrealdb-client-nuget]: https://nuget.org/packages/CommunityToolkit.Aspire.SurrealDb/
[surrealdb-client-shields-preview]: https://img.shields.io/nuget/vpre/CommunityToolkit.Aspire.SurrealDb?label=nuget%20(preview)
[surrealdb-client-nuget-preview]: https://nuget.org/packages/CommunityToolkit.Aspire.SurrealDb/absoluteLatest

Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">

<Sdk Name="Aspire.AppHost.Sdk" Version="$(AspireAppHostSdkVersion)" />

<PropertyGroup>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsAspireHost>true</IsAspireHost>
<UserSecretsId>6445164b-1ed4-461b-baa5-86790b0c158d</UserSecretsId>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Aspire.Hosting.AppHost" />
<PackageReference Include="OpenFeature.Contrib.Providers.Flagd" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="../../../src/CommunityToolkit.Aspire.Hosting.Flagd/CommunityToolkit.Aspire.Hosting.Flagd.csproj" IsAspireProjectResource="false" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using OpenFeature.Contrib.Providers.Flagd;

var builder = DistributedApplication.CreateBuilder(args);

// Add flagd with local flag configuration file
var flagd = builder
.AddFlagd("flagd")
.WithBindFileSync("./flags/")
.WithLogLevel(Microsoft.Extensions.Logging.LogLevel.Debug);

builder.Build().Run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:17255;http://localhost:15238",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21210",
"DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22287"
}
},
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:15238",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19230",
"DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20107"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Aspire.Hosting.Dcp": "Warning"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"$schema": "https://flagd.dev/schema/v0/flags.json",
"flags": {
"welcome-banner": {
"state": "ENABLED",
"variants": {
"on": true,
"off": false
},
"defaultVariant": "on"
},
"background-color": {
"state": "ENABLED",
"variants": {
"red": "#FF0000",
"blue": "#0000FF",
"green": "#00FF00",
"yellow": "#FFFF00"
},
"defaultVariant": "red",
"targeting": {
"if": [
{
"===": [
{
"var": "company"
},
"aspire"
]
},
"blue"
]
}
},
"api-version": {
"state": "ENABLED",
"variants": {
"v1": "1.0",
"v2": "2.0",
"v3": "3.0"
},
"defaultVariant": "v1"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<AdditionalPackageTags>hosting flagd feature-flags openfeature</AdditionalPackageTags>
<Description>flagd is a feature flag evaluation engine. Think of it as a ready-made, open source, OpenFeature-compliant feature flag backend system.</Description>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Aspire.Hosting" />
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="CommunityToolkit.Aspire.Hosting.Flagd.Tests"></InternalsVisibleTo>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using Aspire.Hosting.ApplicationModel;
using CommunityToolkit.Aspire.Hosting.Flagd;
using Microsoft.Extensions.Logging;

namespace Aspire.Hosting;

/// <summary>
/// Provides extension methods for adding flagd resources to an <see cref="IDistributedApplicationBuilder"/>.
/// </summary>
public static class FlagdBuilderExtensions
{
private const int FlagdPort = 8013;
private const int HealthCheckPort = 8014;
private const int OfrepEndpoint = 8016;

/// <summary>
/// Adds a flagd container to the application model.
/// </summary>
/// <param name="builder">The <see cref="IDistributedApplicationBuilder"/>.</param>
/// <param name="name">The name of the resource. This name will be used as the connection string name when referenced in a dependency.</param>
/// <param name="port">The host port for flagd HTTP endpoint. If not provided, a random port will be assigned.</param>
/// <param name="ofrepPort">The host port for flagd OFREP endpoint. If not provided, a random port will be assigned.</param>
/// <remarks>The flagd container requires a sync source to be configured.</remarks>
/// <returns>A reference to the <see cref="IResourceBuilder{FlagdResource}"/>.</returns>
public static IResourceBuilder<FlagdResource> AddFlagd(
this IDistributedApplicationBuilder builder,
[ResourceName] string name,
int? port = null,
int? ofrepPort = null)
{
ArgumentNullException.ThrowIfNull(builder, nameof(builder));
ArgumentException.ThrowIfNullOrEmpty(name, nameof(name));

var resource = new FlagdResource(name);

return builder.AddResource(resource)
.WithImage(FlagdContainerImageTags.Image, FlagdContainerImageTags.Tag)
.WithImageRegistry(FlagdContainerImageTags.Registry)
.WithHttpEndpoint(port: port, targetPort: FlagdPort, name: FlagdResource.HttpEndpointName)
.WithHttpEndpoint(null, HealthCheckPort, FlagdResource.HealthCheckEndpointName)
.WithHttpHealthCheck("/healthz", endpointName: FlagdResource.HealthCheckEndpointName)
.WithHttpEndpoint(ofrepPort, OfrepEndpoint, FlagdResource.OfrepEndpointName)
.WithArgs("start");
}

/// <summary>
/// Configures logging level for flagd. If a flag or targeting rule isn't proceeding the way you'd expect this can be enabled to get more verbose logging.
/// </summary>
/// <param name="builder">The resource builder.</param>
/// <param name="logLevel">The log level to use. Currently only debug is supported.</param>
/// <returns>The <see cref="IResourceBuilder{FlagdResource}"/>.</returns>
/// <exception cref="InvalidOperationException">Thrown if the log level is not valid.</exception>
/// <remarks>Currently only debug is supported.</remarks>
public static IResourceBuilder<FlagdResource> WithLogLevel(
this IResourceBuilder<FlagdResource> builder,
LogLevel logLevel)
{
if (logLevel == LogLevel.Debug)
{
return builder.WithEnvironment("FLAGD_DEBUG", "true");
}

throw new InvalidOperationException("Only debug log level is supported");
}

/// <summary>
/// Configures flagd to use a bind mount as the source of flags.
/// </summary>
/// <param name="builder">The resource builder.</param>
/// <param name="fileSource">The path to the flag configuration file on the host.</param>
/// <param name="filename">The name of the flag configuration file. Defaults to "flagd.json".</param>
/// <returns>The <see cref="IResourceBuilder{FlagdResource}"/>.</returns>
public static IResourceBuilder<FlagdResource> WithBindFileSync(
this IResourceBuilder<FlagdResource> builder,
string fileSource,
string filename = "flagd.json")
{
ArgumentNullException.ThrowIfNull(builder, nameof(builder));
ArgumentException.ThrowIfNullOrEmpty(fileSource, nameof(fileSource));

return builder
.WithBindMount(fileSource, "/flags/")
.WithArgs("--uri", $"file:./flags/{filename}");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace CommunityToolkit.Aspire.Hosting.Flagd;

internal static class FlagdContainerImageTags
{
/// <summary>ghcr.io</summary>
public const string Registry = "ghcr.io";
/// <summary>open-feature/flagd</summary>
public const string Image = "open-feature/flagd";
/// <summary>v0.12.9</summary>
public const string Tag = "v0.12.9";
}
44 changes: 44 additions & 0 deletions src/CommunityToolkit.Aspire.Hosting.Flagd/FlagdResource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
namespace Aspire.Hosting.ApplicationModel;

/// <summary>
/// A resource that represents a flagd container.
/// </summary>
/// <remarks>
/// Constructs a <see cref="FlagdResource"/>.
/// </remarks>
/// <param name="name">The name of the resource.</param>
public class FlagdResource(string name) : ContainerResource(name), IResourceWithConnectionString
{
internal const string HttpEndpointName = "http";
internal const string HealthCheckEndpointName = "health";
internal const string OfrepEndpointName = "ofrep";

private EndpointReference? _primaryEndpointReference;

private EndpointReference? _healthCheckEndpointReference;

private EndpointReference? _ofrepEndpointReference;

/// <summary>
/// Gets the primary HTTP endpoint for the flagd server.
/// </summary>
public EndpointReference PrimaryEndpoint => _primaryEndpointReference ??= new(this, HttpEndpointName);

/// <summary>
/// Gets the health check HTTP endpoint for the flagd server.
/// </summary>
public EndpointReference HealthCheckEndpoint => _healthCheckEndpointReference ??= new(this, HealthCheckEndpointName);

/// <summary>
/// Gets the connection string for the flagd server.
/// </summary>
public EndpointReference OfrepEndpoint => _ofrepEndpointReference ??= new(this, OfrepEndpointName);

/// <summary>
/// Gets the connection string expression for the flagd server.
/// </summary>
public ReferenceExpression ConnectionStringExpression =>
ReferenceExpression.Create(
$"{PrimaryEndpoint.Property(EndpointProperty.Scheme)}://{PrimaryEndpoint.Property(EndpointProperty.Host)}:{PrimaryEndpoint.Property(EndpointProperty.Port)}"
);
}
Loading
Loading