diff --git a/CommunityToolkit.Aspire.sln b/CommunityToolkit.Aspire.sln
index 014aa37c..ca519b7a 100644
--- a/CommunityToolkit.Aspire.sln
+++ b/CommunityToolkit.Aspire.sln
@@ -249,6 +249,22 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hos
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.Dapr.ServiceDefaults", "examples\dapr\CommunityToolkit.Aspire.Hosting.Dapr.ServiceDefaults\CommunityToolkit.Aspire.Hosting.Dapr.ServiceDefaults.csproj", "{99441705-4BFA-499F-9897-371238665E38}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.GoFeatureFlag", "src\CommunityToolkit.Aspire.Hosting.GoFeatureFlag\CommunityToolkit.Aspire.Hosting.GoFeatureFlag.csproj", "{F926FB8A-77C6-4D39-AE44-9A7C11A3202D}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "goff", "goff", "{002E2F54-BB99-41F2-98D5-D6CC3B6A2845}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.GoFeatureFlag.AppHost", "examples\goff\CommunityToolkit.Aspire.Hosting.GoFeatureFlag.AppHost\CommunityToolkit.Aspire.Hosting.GoFeatureFlag.AppHost.csproj", "{FE9F4F1A-1115-4F18-96D8-C8A4AD50CDA5}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.GoFeatureFlag.ServiceDefaults", "examples\goff\CommunityToolkit.Aspire.Hosting.GoFeatureFlag.ServiceDefaults\CommunityToolkit.Aspire.Hosting.GoFeatureFlag.ServiceDefaults.csproj", "{C4C113D8-7E40-4962-BBD8-735B478933D6}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.GoFeatureFlag.ApiService", "examples\goff\CommunityToolkit.Aspire.Hosting.GoFeatureFlag.ApiService\CommunityToolkit.Aspire.Hosting.GoFeatureFlag.ApiService.csproj", "{3CC919A7-3550-4046-99CF-BC164F817E63}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.GoFeatureFlag", "src\CommunityToolkit.Aspire.GoFeatureFlag\CommunityToolkit.Aspire.GoFeatureFlag.csproj", "{C40D5274-DACF-4962-A58A-F08BDA80D0E2}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.GoFeatureFlag.Tests", "tests\CommunityToolkit.Aspire.GoFeatureFlag.Tests\CommunityToolkit.Aspire.GoFeatureFlag.Tests.csproj", "{F4891827-BE07-4547-AF89-2734FB8DE6D5}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.GoFeatureFlag.Tests", "tests\CommunityToolkit.Aspire.Hosting.GoFeatureFlag.Tests\CommunityToolkit.Aspire.Hosting.GoFeatureFlag.Tests.csproj", "{96458513-CAA8-498B-B4EE-3BF994140741}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -655,6 +671,34 @@ Global
{99441705-4BFA-499F-9897-371238665E38}.Debug|Any CPU.Build.0 = Debug|Any CPU
{99441705-4BFA-499F-9897-371238665E38}.Release|Any CPU.ActiveCfg = Release|Any CPU
{99441705-4BFA-499F-9897-371238665E38}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F926FB8A-77C6-4D39-AE44-9A7C11A3202D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F926FB8A-77C6-4D39-AE44-9A7C11A3202D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F926FB8A-77C6-4D39-AE44-9A7C11A3202D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F926FB8A-77C6-4D39-AE44-9A7C11A3202D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FE9F4F1A-1115-4F18-96D8-C8A4AD50CDA5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FE9F4F1A-1115-4F18-96D8-C8A4AD50CDA5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FE9F4F1A-1115-4F18-96D8-C8A4AD50CDA5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FE9F4F1A-1115-4F18-96D8-C8A4AD50CDA5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C4C113D8-7E40-4962-BBD8-735B478933D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C4C113D8-7E40-4962-BBD8-735B478933D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C4C113D8-7E40-4962-BBD8-735B478933D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C4C113D8-7E40-4962-BBD8-735B478933D6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3CC919A7-3550-4046-99CF-BC164F817E63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3CC919A7-3550-4046-99CF-BC164F817E63}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3CC919A7-3550-4046-99CF-BC164F817E63}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3CC919A7-3550-4046-99CF-BC164F817E63}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C40D5274-DACF-4962-A58A-F08BDA80D0E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C40D5274-DACF-4962-A58A-F08BDA80D0E2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C40D5274-DACF-4962-A58A-F08BDA80D0E2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C40D5274-DACF-4962-A58A-F08BDA80D0E2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F4891827-BE07-4547-AF89-2734FB8DE6D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F4891827-BE07-4547-AF89-2734FB8DE6D5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F4891827-BE07-4547-AF89-2734FB8DE6D5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F4891827-BE07-4547-AF89-2734FB8DE6D5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {96458513-CAA8-498B-B4EE-3BF994140741}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {96458513-CAA8-498B-B4EE-3BF994140741}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {96458513-CAA8-498B-B4EE-3BF994140741}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {96458513-CAA8-498B-B4EE-3BF994140741}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -780,6 +824,24 @@ Global
{D2DDEA96-4A7E-496B-AFBE-69A133156C5F} = {E3C2B4B7-B3B0-4E7F-A975-A6C7FD926792}
{5ADBE907-7E0B-4AD7-9073-C032C4183914} = {E3C2B4B7-B3B0-4E7F-A975-A6C7FD926792}
{99441705-4BFA-499F-9897-371238665E38} = {E3C2B4B7-B3B0-4E7F-A975-A6C7FD926792}
+ {BEA41234-DFF9-49AE-AD6C-42A9D54202E7} = {414151D4-7009-4E78-A5C6-D99EBD1E67D1}
+ {6782F1C1-5146-549F-82A8-60C82F1C7F16} = {8519CC01-1370-47C8-AD94-B0F326B1563F}
+ {97E455C1-C914-4C51-87A9-2C213CE2ED5B} = {6782F1C1-5146-549F-82A8-60C82F1C7F16}
+ {5DF8F833-F6F8-4C9C-ABEC-80EC0C734A88} = {414151D4-7009-4E78-A5C6-D99EBD1E67D1}
+ {E48F6DDD-D62D-4723-810D-0F178C35E8B8} = {6782F1C1-5146-549F-82A8-60C82F1C7F16}
+ {DD7042A1-8E44-40A8-B338-DC2F7B755702} = {6782F1C1-5146-549F-82A8-60C82F1C7F16}
+ {E54E9DCA-1420-4306-83B6-D45D6EC49DBF} = {414151D4-7009-4E78-A5C6-D99EBD1E67D1}
+ {0E6EBCFB-DEF5-496C-95AF-00884826CFC8} = {899F0713-7FC6-4750-BAFC-AC650B35B453}
+ {861FE61C-90EE-49B0-BCC8-8417C293CC21} = {899F0713-7FC6-4750-BAFC-AC650B35B453}
+ {52846E18-99D1-4040-AF5F-17FC69198BCE} = {899F0713-7FC6-4750-BAFC-AC650B35B453}
+ {F926FB8A-77C6-4D39-AE44-9A7C11A3202D} = {414151D4-7009-4E78-A5C6-D99EBD1E67D1}
+ {002E2F54-BB99-41F2-98D5-D6CC3B6A2845} = {8519CC01-1370-47C8-AD94-B0F326B1563F}
+ {FE9F4F1A-1115-4F18-96D8-C8A4AD50CDA5} = {002E2F54-BB99-41F2-98D5-D6CC3B6A2845}
+ {C4C113D8-7E40-4962-BBD8-735B478933D6} = {002E2F54-BB99-41F2-98D5-D6CC3B6A2845}
+ {3CC919A7-3550-4046-99CF-BC164F817E63} = {002E2F54-BB99-41F2-98D5-D6CC3B6A2845}
+ {C40D5274-DACF-4962-A58A-F08BDA80D0E2} = {414151D4-7009-4E78-A5C6-D99EBD1E67D1}
+ {F4891827-BE07-4547-AF89-2734FB8DE6D5} = {899F0713-7FC6-4750-BAFC-AC650B35B453}
+ {96458513-CAA8-498B-B4EE-3BF994140741} = {899F0713-7FC6-4750-BAFC-AC650B35B453}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {08B1D4B8-D2C5-4A64-BB8B-E1A2B29525F0}
diff --git a/Directory.Packages.props b/Directory.Packages.props
index bfc3262e..69070d3a 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -16,6 +16,7 @@
+
@@ -53,6 +54,7 @@
+
diff --git a/README.md b/README.md
index 7e08344b..34421955 100644
--- a/README.md
+++ b/README.md
@@ -36,6 +36,8 @@ This repository contains the source code for the .NET Aspire Community Toolkit,
| - **Learn More**: [`Microsoft.EntityFrameworkCore.Sqlite`][sqlite-ef-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Microsoft.EntityFrameworkCore.Sqlite][sqlite-ef-shields]][sqlite-ef-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Microsoft.EntityFrameworkCore.Sqlite][sqlite-ef-shields-preview]][sqlite-ef-nuget-preview] | An Aspire client integration for the Microsoft.EntityFrameworkCore.Sqlite NuGet package. |
| - **Learn More**: [`Hosting.Dapr`][dapr-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.Dapr][dapr-shields]][dapr-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Dapr][dapr-shields-preview]][dapr-nuget-preview] | An Aspire hosting integration for Dapr. |
| - **Learn More**: [`Hosting.Dapr.AzureRedis`][dapr-azureredis-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis][dapr-azureredis-shields]][dapr-azureredis-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis][dapr-azureredis-shields-preview]][dapr-azureredis-nuget-preview] | An extension for the Dapr hosting integration for using Dapr with Azure Redis cache. |
+| - **Learn More**: [`Hosting.GoFeatureFlag`][go-feature-flag-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.GoFeatureFlag][go-feature-flag-shields]][go-feature-flag-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.GoFeatureFlag][go-feature-flag-shields-preview]][go-feature-flag-nuget-preview] | An Aspire hosting integration leveraging the [GoFeatureFlag](https://gofeatureflag.org/) container. |
+| - **Learn More**: [`GoFeatureFlag`][go-feature-flag-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.GoFeatureFlag][go-feature-flag-client-shields]][go-feature-flag-client-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.GoFeatureFlag][go-feature-flag-client-shields-preview]][go-feature-flag-client-nuget-preview] | An Aspire client integration for the [GoFeatureFlag](https://github.com/open-feature/dotnet-sdk-contrib/tree/main/src/OpenFeature.Contrib.Providers.GOFeatureFlag) package. |
## 🙌 Getting Started
@@ -182,3 +184,12 @@ This project is supported by the [.NET Foundation](https://dotnetfoundation.org)
[dapr-azureredis-nuget]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis/
[dapr-azureredis-shields-preview]: https://img.shields.io/nuget/vpre/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis?label=nuget%20(preview)
[dapr-azureredis-nuget-preview]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis/absoluteLatest
+[go-feature-flag-integration-docs]: https://learn.microsoft.com/dotnet/aspire/community-toolkit/hosting-go-feature-flag
+[go-feature-flag-shields]: https://img.shields.io/nuget/v/CommunityToolkit.Aspire.Hosting.GoFeatureFlag
+[go-feature-flag-nuget]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.GoFeatureFlag/
+[go-feature-flag-shields-preview]: https://img.shields.io/nuget/vpre/CommunityToolkit.Aspire.Hosting.GoFeatureFlag?label=nuget%20(preview)
+[go-feature-flag-nuget-preview]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.GoFeatureFlag/absoluteLatest
+[go-feature-flag-client-shields]: https://img.shields.io/nuget/v/CommunityToolkit.Aspire.GoFeatureFlag
+[go-feature-flag-client-nuget]: https://nuget.org/packages/CommunityToolkit.Aspire.GoFeatureFlag/
+[go-feature-flag-client-shields-preview]: https://img.shields.io/nuget/vpre/CommunityToolkit.Aspire.GoFeatureFlag?label=nuget%20(preview)
+[go-feature-flag-client-nuget-preview]: https://nuget.org/packages/CommunityToolkit.Aspire.GoFeatureFlag/absoluteLatest
diff --git a/examples/goff/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.ApiService/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.ApiService.csproj b/examples/goff/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.ApiService/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.ApiService.csproj
new file mode 100644
index 00000000..30868a73
--- /dev/null
+++ b/examples/goff/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.ApiService/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.ApiService.csproj
@@ -0,0 +1,13 @@
+
+
+
+ enable
+ enable
+
+
+
+
+
+
+
+
diff --git a/examples/goff/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.ApiService/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.ApiService.http b/examples/goff/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.ApiService/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.ApiService.http
new file mode 100644
index 00000000..df3db550
--- /dev/null
+++ b/examples/goff/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.ApiService/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.ApiService.http
@@ -0,0 +1,6 @@
+@CommunityToolkit.Aspire.Hosting.GoFeatureFlag.ApiService_HostAddress = http://localhost:5100
+
+GET {{CommunityToolkit.Aspire.Hosting.GoFeatureFlag.ApiService_HostAddress}}/weatherforecast/
+Accept: application/json
+
+###
diff --git a/examples/goff/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.ApiService/Program.cs b/examples/goff/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.ApiService/Program.cs
new file mode 100644
index 00000000..3e1ad1a4
--- /dev/null
+++ b/examples/goff/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.ApiService/Program.cs
@@ -0,0 +1,30 @@
+using OpenFeature.Contrib.Providers.GOFeatureFlag;
+using OpenFeature.Model;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.AddServiceDefaults();
+
+builder.AddGoFeatureFlagClient("goff");
+
+var app = builder.Build();
+
+app.MapDefaultEndpoints();
+
+// Currently supported flags are:
+// - `display-banner`
+app.MapGet(
+ "/features/{featureName}",
+ async (string featureName, GoFeatureFlagProvider provider, CancellationToken cancellationToken) =>
+ {
+ var userContext = EvaluationContext.Builder()
+ .Set("targetingKey", Guid.NewGuid().ToString())
+ .Set("anonymous", true)
+ .Build();
+ var flag = await provider.ResolveBooleanValueAsync(featureName, false, userContext, cancellationToken);
+
+ return Results.Ok(flag);
+ })
+ .WithName("GetFeature");
+
+app.Run();
\ No newline at end of file
diff --git a/examples/goff/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.ApiService/Properties/launchSettings.json b/examples/goff/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.ApiService/Properties/launchSettings.json
new file mode 100644
index 00000000..8c119a07
--- /dev/null
+++ b/examples/goff/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.ApiService/Properties/launchSettings.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "applicationUrl": "http://localhost:5100",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "applicationUrl": "https://localhost:7101;http://localhost:5100",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/examples/goff/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.ApiService/appsettings.json b/examples/goff/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.ApiService/appsettings.json
new file mode 100644
index 00000000..10f68b8c
--- /dev/null
+++ b/examples/goff/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.ApiService/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/examples/goff/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.AppHost/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.AppHost.csproj b/examples/goff/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.AppHost/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.AppHost.csproj
new file mode 100644
index 00000000..cb322efb
--- /dev/null
+++ b/examples/goff/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.AppHost/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.AppHost.csproj
@@ -0,0 +1,21 @@
+
+
+
+
+ Exe
+ enable
+ enable
+ true
+ 6f09f62a-98fa-44a3-a1ac-a3ae097e367f
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/goff/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.AppHost/Program.cs b/examples/goff/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.AppHost/Program.cs
new file mode 100644
index 00000000..911ec40d
--- /dev/null
+++ b/examples/goff/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.AppHost/Program.cs
@@ -0,0 +1,13 @@
+using Projects;
+
+var builder = DistributedApplication.CreateBuilder(args);
+
+var goff = builder.AddGoFeatureFlag("goff")
+ .WithGoffBindMount("./goff");
+
+builder.AddProject("apiservice")
+ .WithReference(goff)
+ .WaitFor(goff)
+ .WithHttpHealthCheck("/health");
+
+builder.Build().Run();
\ No newline at end of file
diff --git a/examples/goff/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.AppHost/Properties/launchSettings.json b/examples/goff/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.AppHost/Properties/launchSettings.json
new file mode 100644
index 00000000..f6132e4c
--- /dev/null
+++ b/examples/goff/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.AppHost/Properties/launchSettings.json
@@ -0,0 +1,29 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:17114;http://localhost:15226",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21156",
+ "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22092"
+ }
+ },
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:15226",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19237",
+ "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20145"
+ }
+ }
+ }
+}
diff --git a/examples/goff/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.AppHost/appsettings.json b/examples/goff/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.AppHost/appsettings.json
new file mode 100644
index 00000000..31c092aa
--- /dev/null
+++ b/examples/goff/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.AppHost/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning",
+ "Aspire.Hosting.Dcp": "Warning"
+ }
+ }
+}
diff --git a/examples/goff/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.AppHost/goff/flags.yaml b/examples/goff/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.AppHost/goff/flags.yaml
new file mode 100644
index 00000000..b3efd1f2
--- /dev/null
+++ b/examples/goff/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.AppHost/goff/flags.yaml
@@ -0,0 +1,6 @@
+display-banner:
+ variations:
+ enabled: true
+ disabled: false
+ defaultRule:
+ variation: enabled
diff --git a/examples/goff/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.AppHost/goff/goff-proxy.yaml b/examples/goff/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.AppHost/goff/goff-proxy.yaml
new file mode 100644
index 00000000..4a373a32
--- /dev/null
+++ b/examples/goff/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.AppHost/goff/goff-proxy.yaml
@@ -0,0 +1,3 @@
+retrievers:
+ - kind: file
+ path: /goff/flags.yaml
diff --git a/examples/goff/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.ServiceDefaults/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.ServiceDefaults.csproj b/examples/goff/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.ServiceDefaults/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.ServiceDefaults.csproj
new file mode 100644
index 00000000..caa6344d
--- /dev/null
+++ b/examples/goff/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.ServiceDefaults/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.ServiceDefaults.csproj
@@ -0,0 +1,21 @@
+
+
+
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/goff/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.ServiceDefaults/Extensions.cs b/examples/goff/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.ServiceDefaults/Extensions.cs
new file mode 100644
index 00000000..359eaa17
--- /dev/null
+++ b/examples/goff/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.ServiceDefaults/Extensions.cs
@@ -0,0 +1,115 @@
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Diagnostics.HealthChecks;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.ServiceDiscovery;
+using OpenTelemetry;
+using OpenTelemetry.Metrics;
+using OpenTelemetry.Trace;
+
+namespace Microsoft.Extensions.Hosting;
+
+// Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry.
+// This project should be referenced by each service project in your solution.
+// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults
+public static class Extensions
+{
+ public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder)
+ {
+ builder.ConfigureOpenTelemetry();
+
+ builder.AddDefaultHealthChecks();
+
+ builder.Services.AddServiceDiscovery();
+
+ builder.Services.ConfigureHttpClientDefaults(http =>
+ {
+ // Turn on resilience by default
+ http.AddStandardResilienceHandler();
+
+ // Turn on service discovery by default
+ http.AddServiceDiscovery();
+ });
+
+ // Uncomment the following to restrict the allowed schemes for service discovery.
+ // builder.Services.Configure(options =>
+ // {
+ // options.AllowedSchemes = ["https"];
+ // });
+
+ return builder;
+ }
+
+ public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder)
+ {
+ builder.Logging.AddOpenTelemetry(logging =>
+ {
+ logging.IncludeFormattedMessage = true;
+ logging.IncludeScopes = true;
+ });
+
+ builder.Services.AddOpenTelemetry()
+ .WithMetrics(metrics =>
+ {
+ metrics.AddAspNetCoreInstrumentation()
+ .AddHttpClientInstrumentation()
+ .AddRuntimeInstrumentation();
+ })
+ .WithTracing(tracing =>
+ {
+ tracing.AddAspNetCoreInstrumentation()
+ // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package)
+ //.AddGrpcClientInstrumentation()
+ .AddHttpClientInstrumentation();
+ });
+
+ builder.AddOpenTelemetryExporters();
+
+ return builder;
+ }
+
+ private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder)
+ {
+ var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);
+
+ if (useOtlpExporter)
+ {
+ builder.Services.AddOpenTelemetry().UseOtlpExporter();
+ }
+
+ // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package)
+ //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"]))
+ //{
+ // builder.Services.AddOpenTelemetry()
+ // .UseAzureMonitor();
+ //}
+
+ return builder;
+ }
+
+ public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder)
+ {
+ builder.Services.AddHealthChecks()
+ // Add a default liveness check to ensure app is responsive
+ .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]);
+
+ return builder;
+ }
+
+ public static WebApplication MapDefaultEndpoints(this WebApplication app)
+ {
+ // Adding health checks endpoints to applications in non-development environments has security implications.
+ // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments.
+ if (app.Environment.IsDevelopment())
+ {
+ // All health checks must pass for app to be considered ready to accept traffic after starting
+ app.MapHealthChecks("/health");
+
+ // Only health checks tagged with the "live" tag must pass for app to be considered alive
+ app.MapHealthChecks("/alive", new HealthCheckOptions { Predicate = r => r.Tags.Contains("live") });
+ }
+
+ return app;
+ }
+}
\ No newline at end of file
diff --git a/src/CommunityToolkit.Aspire.GoFeatureFlag/AspireGoFeatureFlagExtensions.cs b/src/CommunityToolkit.Aspire.GoFeatureFlag/AspireGoFeatureFlagExtensions.cs
new file mode 100644
index 00000000..d0974931
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.GoFeatureFlag/AspireGoFeatureFlagExtensions.cs
@@ -0,0 +1,121 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire;
+using CommunityToolkit.Aspire.GoFeatureFlag;
+using HealthChecks.Uris;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+using OpenFeature.Contrib.Providers.GOFeatureFlag;
+
+namespace Microsoft.Extensions.Hosting;
+
+///
+/// Provides extension methods for registering GO Feature Flag-related services in an .
+///
+public static class AspireGoFeatureFlagExtensions
+{
+ private const string DefaultConfigSectionName = "Aspire:GoFeatureFlag:Client";
+
+ ///
+ /// Registers as a singleton in the services provided by the .
+ ///
+ /// The to read config from and add services to.
+ /// The connection name to use to find a connection string.
+ /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration.
+ /// Reads the configuration from "Aspire:GoFeatureFlag:Client" section.
+ /// If required ConnectionString is not provided in configuration section
+ public static void AddGoFeatureFlagClient(
+ this IHostApplicationBuilder builder,
+ string connectionName,
+ Action? configureSettings = null)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNullOrEmpty(connectionName);
+ AddGoFeatureFlagClient(builder, DefaultConfigSectionName, configureSettings, connectionName, serviceKey: null);
+ }
+
+ ///
+ /// Registers as a keyed singleton for the given in the services provided by the .
+ ///
+ /// The to read config from and add services to.
+ /// The connection name to use to find a connection string.
+ /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration.
+ /// Reads the configuration from "Aspire:GoFeatureFlag:Client" section.
+ /// If required ConnectionString is not provided in configuration section
+ public static void AddKeyedGoFeatureFlagClient(
+ this IHostApplicationBuilder builder,
+ string name,
+ Action? configureSettings = null)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNullOrEmpty(name);
+ AddGoFeatureFlagClient(builder, $"{DefaultConfigSectionName}:{name}", configureSettings, connectionName: name, serviceKey: name);
+ }
+
+ private static void AddGoFeatureFlagClient(
+ this IHostApplicationBuilder builder,
+ string configurationSectionName,
+ Action? configureSettings,
+ string connectionName,
+ string? serviceKey)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+
+ var settings = new GoFeatureFlagClientSettings();
+
+ if (builder.Configuration.GetConnectionString(connectionName) is string connectionString)
+ {
+ settings.ParseConnectionString(connectionString);
+ }
+
+ configureSettings?.Invoke(settings);
+
+ if (settings.Endpoint is not null && string.IsNullOrEmpty(settings.ProviderOptions.Endpoint))
+ {
+ settings.ProviderOptions.Endpoint = settings.Endpoint.ToString();
+ }
+
+ if (serviceKey is null)
+ {
+ builder.Services.AddSingleton((sp) => ConfigureGoFeatureFlagClient(settings.ProviderOptions));
+ }
+ else
+ {
+ builder.Services.AddKeyedSingleton(serviceKey, (sp, key) => ConfigureGoFeatureFlagClient(settings.ProviderOptions));
+ }
+
+ if (settings is { DisableHealthChecks: false, Endpoint: not null })
+ {
+ var healthCheckName = serviceKey is null ? "Goff" : $"Goff_{connectionName}";
+
+ var healthEndpoint = new Uri(settings.Endpoint, "/health");
+ var uriHealthCheck = new UriHealthCheck(
+ new UriHealthCheckOptions().AddUri(healthEndpoint),
+ () => new HttpClient()
+ );
+
+ builder.TryAddHealthCheck(new HealthCheckRegistration(
+ healthCheckName,
+ sp => uriHealthCheck,
+ failureStatus: null,
+ tags: null,
+ timeout: settings.HealthCheckTimeout > 0 ? TimeSpan.FromMilliseconds(settings.HealthCheckTimeout.Value) : null
+ ));
+ }
+
+ GoFeatureFlagProvider ConfigureGoFeatureFlagClient(GoFeatureFlagProviderOptions options)
+ {
+ if (settings.Endpoint is not null)
+ {
+ return new GoFeatureFlagProvider(options);
+ }
+
+ throw new InvalidOperationException(
+ $"A GoFeatureFlagProvider could not be configured. Ensure valid connection information was provided in 'ConnectionStrings:{connectionName}' or either " +
+ $"{nameof(settings.Endpoint)} must be provided " +
+ $"in the '{configurationSectionName}' configuration section.");
+ }
+ }
+}
diff --git a/src/CommunityToolkit.Aspire.GoFeatureFlag/CommunityToolkit.Aspire.GoFeatureFlag.csproj b/src/CommunityToolkit.Aspire.GoFeatureFlag/CommunityToolkit.Aspire.GoFeatureFlag.csproj
new file mode 100644
index 00000000..50727d32
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.GoFeatureFlag/CommunityToolkit.Aspire.GoFeatureFlag.csproj
@@ -0,0 +1,21 @@
+
+
+
+ GoFeatureFlag client
+ A GO Feature Flag client that integrates with Aspire, including health checks, logging, and telemetry.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/CommunityToolkit.Aspire.GoFeatureFlag/GoFeatureFlagClientSettings.cs b/src/CommunityToolkit.Aspire.GoFeatureFlag/GoFeatureFlagClientSettings.cs
new file mode 100644
index 00000000..a99abe87
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.GoFeatureFlag/GoFeatureFlagClientSettings.cs
@@ -0,0 +1,58 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using OpenFeature.Contrib.Providers.GOFeatureFlag;
+using System.Data.Common;
+
+namespace CommunityToolkit.Aspire.GoFeatureFlag;
+
+///
+/// Provides the client configuration settings for connecting to a GO Feature Flag server.
+///
+public sealed class GoFeatureFlagClientSettings
+{
+ private const string ConnectionStringEndpoint = "Endpoint";
+
+ ///
+ /// The endpoint URI string of the GO Feature Flag server to connect to.
+ ///
+ public Uri? Endpoint { get; set; }
+
+ ///
+ /// Gets or sets a boolean value that indicates whether the GO Feature Flag health check is disabled or not.
+ ///
+ ///
+ /// The default value is .
+ ///
+ public bool DisableHealthChecks { get; set; }
+
+ ///
+ /// Gets or sets a integer value that indicates the GO Feature Flag health check timeout in milliseconds.
+ ///
+ public int? HealthCheckTimeout { get; set; }
+
+ ///
+ /// Gets or sets the provider options that will be used to configure the GO Feature Flag client.
+ ///
+ public GoFeatureFlagProviderOptions ProviderOptions { get; set; } = new();
+
+ internal void ParseConnectionString(string? connectionString)
+ {
+ if (Uri.TryCreate(connectionString, UriKind.Absolute, out var uri))
+ {
+ Endpoint = uri;
+ }
+ else
+ {
+ var connectionBuilder = new DbConnectionStringBuilder
+ {
+ ConnectionString = connectionString
+ };
+
+ if (connectionBuilder.TryGetValue(ConnectionStringEndpoint, out var endpoint) && Uri.TryCreate(endpoint.ToString(), UriKind.Absolute, out var serviceUri))
+ {
+ Endpoint = serviceUri;
+ }
+ }
+ }
+}
diff --git a/src/CommunityToolkit.Aspire.GoFeatureFlag/PublicAPI.Shipped.txt b/src/CommunityToolkit.Aspire.GoFeatureFlag/PublicAPI.Shipped.txt
new file mode 100644
index 00000000..7dc5c581
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.GoFeatureFlag/PublicAPI.Shipped.txt
@@ -0,0 +1 @@
+#nullable enable
diff --git a/src/CommunityToolkit.Aspire.GoFeatureFlag/PublicAPI.Unshipped.txt b/src/CommunityToolkit.Aspire.GoFeatureFlag/PublicAPI.Unshipped.txt
new file mode 100644
index 00000000..e9538fa0
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.GoFeatureFlag/PublicAPI.Unshipped.txt
@@ -0,0 +1,14 @@
+#nullable enable
+CommunityToolkit.Aspire.GoFeatureFlag.GoFeatureFlagClientSettings
+CommunityToolkit.Aspire.GoFeatureFlag.GoFeatureFlagClientSettings.DisableHealthChecks.get -> bool
+CommunityToolkit.Aspire.GoFeatureFlag.GoFeatureFlagClientSettings.DisableHealthChecks.set -> void
+CommunityToolkit.Aspire.GoFeatureFlag.GoFeatureFlagClientSettings.Endpoint.get -> System.Uri?
+CommunityToolkit.Aspire.GoFeatureFlag.GoFeatureFlagClientSettings.Endpoint.set -> void
+CommunityToolkit.Aspire.GoFeatureFlag.GoFeatureFlagClientSettings.GoFeatureFlagClientSettings() -> void
+CommunityToolkit.Aspire.GoFeatureFlag.GoFeatureFlagClientSettings.HealthCheckTimeout.get -> int?
+CommunityToolkit.Aspire.GoFeatureFlag.GoFeatureFlagClientSettings.HealthCheckTimeout.set -> void
+CommunityToolkit.Aspire.GoFeatureFlag.GoFeatureFlagClientSettings.ProviderOptions.get -> OpenFeature.Contrib.Providers.GOFeatureFlag.GoFeatureFlagProviderOptions!
+CommunityToolkit.Aspire.GoFeatureFlag.GoFeatureFlagClientSettings.ProviderOptions.set -> void
+Microsoft.Extensions.Hosting.AspireGoFeatureFlagExtensions
+static Microsoft.Extensions.Hosting.AspireGoFeatureFlagExtensions.AddGoFeatureFlagClient(this Microsoft.Extensions.Hosting.IHostApplicationBuilder! builder, string! connectionName, System.Action? configureSettings = null) -> void
+static Microsoft.Extensions.Hosting.AspireGoFeatureFlagExtensions.AddKeyedGoFeatureFlagClient(this Microsoft.Extensions.Hosting.IHostApplicationBuilder! builder, string! name, System.Action? configureSettings = null) -> void
diff --git a/src/CommunityToolkit.Aspire.GoFeatureFlag/README.md b/src/CommunityToolkit.Aspire.GoFeatureFlag/README.md
new file mode 100644
index 00000000..43d2d841
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.GoFeatureFlag/README.md
@@ -0,0 +1,109 @@
+# CommunityToolkit.Aspire.GoFeatureFlag
+
+Registers a [GoFeatureFlagProvider](https://github.com/open-feature/dotnet-sdk-contrib/tree/main/src/OpenFeature.Contrib.Providers.GOFeatureFlag) in the DI container for connecting to a GO Feature Flag instance.
+
+## Getting started
+
+### Install the package
+
+Install the .NET Aspire GO Feature Flag Client library with [NuGet](https://www.nuget.org):
+
+```dotnetcli
+dotnet add package CommunityToolkit.Aspire.GoFeatureFlag
+```
+
+## Usage example
+
+In the _Program.cs_ file of your project, call the `AddGoFeatureFlagClient` extension method to register a `GoFeatureFlagProvider` for use via the dependency injection container. The method takes a connection name parameter.
+
+```csharp
+builder.AddGoFeatureFlagClient("goff");
+```
+
+## Configuration
+
+The .NET Aspire GO Feature Flag Client integration provides multiple options to configure the server connection based on the requirements and conventions of your project.
+
+### Use a connection string
+
+When using a connection string from the `ConnectionStrings` configuration section, you can provide the name of the connection string when calling `builder.AddGoFeatureFlagClient()`:
+
+```csharp
+builder.AddGoFeatureFlagClient("goff");
+```
+
+And then the connection string will be retrieved from the `ConnectionStrings` configuration section:
+
+```json
+{
+ "ConnectionStrings": {
+ "goff": "Endpoint=http://localhost:19530/"
+ }
+}
+```
+
+### Use configuration providers
+
+The .NET Aspire GO Feature Flag Client integration supports [Microsoft.Extensions.Configuration](https://learn.microsoft.com/dotnet/api/microsoft.extensions.configuration). It loads the `GoFeatureFlagClientSettings` from configuration by using the `Aspire:GoFeatureFlag:Client` key. Example `appsettings.json` that configures some of the options:
+
+```json
+{
+ "Aspire": {
+ "GoFeatureFlag": {
+ "Client": {
+ "Endpoint": "http://localhost:19530/",
+ "MasterKey": "123456!@#$%"
+ }
+ }
+ }
+}
+```
+
+### Use inline delegates
+
+Also you can pass the `Action configureSettings` delegate to set up some or all the options inline, for example to set the API key from code:
+
+```csharp
+builder.AddGoFeatureFlagClient("goff", settings => settings.ProviderOptions.ApiKey = "123456!@#$%");
+```
+
+## AppHost extensions
+
+In your AppHost project, install the `CommunityToolkit.Aspire.Hosting.GoFeatureFlag` library with [NuGet](https://www.nuget.org):
+
+```dotnetcli
+dotnet add package CommunityToolkit.Aspire.Hosting.GoFeatureFlag
+```
+
+Then, in the _Program.cs_ file of `AppHost`, register a GO Feature Flag instance and consume the connection using the following methods:
+
+```csharp
+var goff = builder.AddGoFeatureFlag("goff");
+
+var myService = builder.AddProject()
+ .WithReference(goff);
+```
+
+The `WithReference` method configures a connection in the `MyService` project named `goff`. In the _Program.cs_ file of `MyService`, the GO Feature Flag connection can be consumed using:
+
+```csharp
+builder.AddGoFeatureFlagClient("goff");
+```
+
+Then, in your service, inject `GoFeatureFlagProvider` and use it to interact with the GO Feature Flag API:
+
+```csharp
+public class MyService(GoFeatureFlagProvider goFeatureFlagProvider)
+{
+ // ...
+}
+```
+
+## Additional documentation
+
+- https://github.com/thomaspoignant/go-feature-flag
+- https://learn.microsoft.com/dotnet/aspire/community-toolkit/hosting-go-feature-flag
+
+## Feedback & contributing
+
+https://github.com/CommunityToolkit/Aspire
diff --git a/src/CommunityToolkit.Aspire.Hosting.GoFeatureFlag/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.csproj b/src/CommunityToolkit.Aspire.Hosting.GoFeatureFlag/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.csproj
new file mode 100644
index 00000000..03f723c9
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.GoFeatureFlag/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.csproj
@@ -0,0 +1,16 @@
+
+
+
+ hosting gofeatureflag
+ GO Feature Flag support for .NET Aspire.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/CommunityToolkit.Aspire.Hosting.GoFeatureFlag/GoFeatureFlagBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.GoFeatureFlag/GoFeatureFlagBuilderExtensions.cs
new file mode 100644
index 00000000..ed6caa75
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.GoFeatureFlag/GoFeatureFlagBuilderExtensions.cs
@@ -0,0 +1,125 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Hosting.ApplicationModel;
+using Aspire.Hosting.Utils;
+using CommunityToolkit.Aspire.Hosting.GoFeatureFlag;
+
+namespace Aspire.Hosting;
+
+///
+/// Provides extension methods for adding GO Feature Flag resources to the application model.
+///
+public static class GoFeatureFlagBuilderExtensions
+{
+ private const int GoFeatureFlagPort = 1031;
+
+ ///
+ /// Adds an GO Feature Flag container resource to the application model.
+ /// The default image is and the tag is .
+ ///
+ /// The .
+ /// The name of the resource. This name will be used as the connection string name when referenced in a dependency.
+ /// The path set to find the configuration file (https://gofeatureflag.org/docs/relay-proxy/configure-relay-proxy#configuration-file).
+ /// The host port to bind the underlying container to.
+ /// A reference to the .
+ ///
+ ///
+ /// Add an GO Feature Flag container to the application model and reference it in a .NET project.
+ ///
+ /// var builder = DistributedApplication.CreateBuilder(args);
+ ///
+ /// var goff = builder.AddGoFeatureFlag("goff");
+ /// var api = builder.AddProject<Projects.Api>("api")
+ /// .WithReference(goff);
+ ///
+ /// builder.Build().Run();
+ ///
+ ///
+ ///
+ public static IResourceBuilder AddGoFeatureFlag(
+ this IDistributedApplicationBuilder builder,
+ string name,
+ string? pathToConfigFile = null,
+ int? port = null)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(name);
+
+ var goFeatureFlagResource = new GoFeatureFlagResource(name);
+
+ string[] args = string.IsNullOrWhiteSpace(pathToConfigFile)
+ ? []
+ : [$"--config={pathToConfigFile}"];
+
+ return builder.AddResource(goFeatureFlagResource)
+ .WithImage(GoFeatureFlagContainerImageTags.Image, GoFeatureFlagContainerImageTags.Tag)
+ .WithImageRegistry(GoFeatureFlagContainerImageTags.Registry)
+ .WithHttpEndpoint(targetPort: GoFeatureFlagPort, port: port, name: GoFeatureFlagResource.PrimaryEndpointName)
+ .WithHttpHealthCheck("/health")
+ .WithEntrypoint("/go-feature-flag")
+ .WithArgs(args)
+ .WithOtlpExporter();
+ }
+
+ ///
+ /// Adds a named volume for the data folder to a GO Feature flag container resource.
+ ///
+ /// The resource builder.
+ /// The name of the volume. Defaults to an auto-generated name based on the application and resource names.
+ /// The .
+ ///
+ ///
+ /// Add a GO Feature flag container to the application model and reference it in a .NET project. Additionally, in this
+ /// example a data volume is added to the container to allow data to be persisted across container restarts.
+ ///
+ /// var builder = DistributedApplication.CreateBuilder(args);
+ ///
+ /// var goff = builder.AddGoFeatureFlag("goff")
+ /// .WithDataVolume();
+ /// var api = builder.AddProject<Projects.Api>("api")
+ /// .WithReference(goff);
+ ///
+ /// builder.Build().Run();
+ ///
+ ///
+ ///
+ public static IResourceBuilder WithDataVolume(this IResourceBuilder builder, string? name = null)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+
+#pragma warning disable CTASPIRE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
+ return builder.WithVolume(name ?? VolumeNameGenerator.CreateVolumeName(builder, "data"), "/goff_data");
+#pragma warning restore CTASPIRE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
+ }
+
+ ///
+ /// Adds a bind mount for the goff configuration folder to a GO Feature flag container resource.
+ ///
+ /// The resource builder.
+ /// The source directory on the host to mount into the container.
+ /// The .
+ ///
+ ///
+ /// Add a GO Feature flag container to the application model and reference it in a .NET project. Additionally, in this
+ /// example a bind mount is added to the container to allow reading goff configuration.
+ ///
+ /// var builder = DistributedApplication.CreateBuilder(args);
+ ///
+ /// var goff = builder.AddGoFeatureFlag("goff")
+ /// .WithGoffBindMount("./goff");
+ /// var api = builder.AddProject<Projects.Api>("api")
+ /// .WithReference(goff);
+ ///
+ /// builder.Build().Run();
+ ///
+ ///
+ ///
+ public static IResourceBuilder WithGoffBindMount(this IResourceBuilder builder, string source)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(source);
+
+ return builder.WithBindMount(source, "/goff");
+ }
+}
\ No newline at end of file
diff --git a/src/CommunityToolkit.Aspire.Hosting.GoFeatureFlag/GoFeatureFlagContainerImageTags.cs b/src/CommunityToolkit.Aspire.Hosting.GoFeatureFlag/GoFeatureFlagContainerImageTags.cs
new file mode 100644
index 00000000..6c196b0f
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.GoFeatureFlag/GoFeatureFlagContainerImageTags.cs
@@ -0,0 +1,14 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace CommunityToolkit.Aspire.Hosting.GoFeatureFlag;
+
+internal static class GoFeatureFlagContainerImageTags
+{
+ /// docker.io
+ public const string Registry = "docker.io";
+ /// gofeatureflag/go-feature-flag
+ public const string Image = "gofeatureflag/go-feature-flag";
+ /// v1.41
+ public const string Tag = "v1.41";
+}
\ No newline at end of file
diff --git a/src/CommunityToolkit.Aspire.Hosting.GoFeatureFlag/GoFeatureFlagResource.cs b/src/CommunityToolkit.Aspire.Hosting.GoFeatureFlag/GoFeatureFlagResource.cs
new file mode 100644
index 00000000..5d994ddb
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.GoFeatureFlag/GoFeatureFlagResource.cs
@@ -0,0 +1,26 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Aspire.Hosting.ApplicationModel;
+
+///
+/// A resource that represents a GO Feature Flag instance
+///
+public class GoFeatureFlagResource(string name) : ContainerResource(name), IResourceWithConnectionString
+{
+ internal const string PrimaryEndpointName = "http";
+
+ private EndpointReference? _primaryEndpoint;
+
+ ///
+ /// Gets the primary endpoint for the GO Feature Flag instance.
+ /// This endpoint is used for all API calls over HTTP.
+ ///
+ public EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new(this, PrimaryEndpointName);
+
+ ///
+ /// Gets the connection string expression for the GO Feature Flag instance.
+ ///
+ public ReferenceExpression ConnectionStringExpression =>
+ ReferenceExpression.Create($"Endpoint=http://{PrimaryEndpoint.Property(EndpointProperty.Host)}:{PrimaryEndpoint.Property(EndpointProperty.Port)}");
+}
\ No newline at end of file
diff --git a/src/CommunityToolkit.Aspire.Hosting.GoFeatureFlag/PublicAPI.Shipped.txt b/src/CommunityToolkit.Aspire.Hosting.GoFeatureFlag/PublicAPI.Shipped.txt
new file mode 100644
index 00000000..7dc5c581
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.GoFeatureFlag/PublicAPI.Shipped.txt
@@ -0,0 +1 @@
+#nullable enable
diff --git a/src/CommunityToolkit.Aspire.Hosting.GoFeatureFlag/PublicAPI.Unshipped.txt b/src/CommunityToolkit.Aspire.Hosting.GoFeatureFlag/PublicAPI.Unshipped.txt
new file mode 100644
index 00000000..f04bd19d
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.GoFeatureFlag/PublicAPI.Unshipped.txt
@@ -0,0 +1,9 @@
+#nullable enable
+Aspire.Hosting.ApplicationModel.GoFeatureFlagResource
+Aspire.Hosting.ApplicationModel.GoFeatureFlagResource.ConnectionStringExpression.get -> Aspire.Hosting.ApplicationModel.ReferenceExpression!
+Aspire.Hosting.ApplicationModel.GoFeatureFlagResource.GoFeatureFlagResource(string! name) -> void
+Aspire.Hosting.ApplicationModel.GoFeatureFlagResource.PrimaryEndpoint.get -> Aspire.Hosting.ApplicationModel.EndpointReference!
+Aspire.Hosting.GoFeatureFlagBuilderExtensions
+static Aspire.Hosting.GoFeatureFlagBuilderExtensions.AddGoFeatureFlag(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, string? pathToConfigFile = null, int? port = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder!
+static Aspire.Hosting.GoFeatureFlagBuilderExtensions.WithDataVolume(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string? name = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder!
+static Aspire.Hosting.GoFeatureFlagBuilderExtensions.WithGoffBindMount(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string! source) -> Aspire.Hosting.ApplicationModel.IResourceBuilder!
\ No newline at end of file
diff --git a/src/CommunityToolkit.Aspire.Hosting.GoFeatureFlag/README.md b/src/CommunityToolkit.Aspire.Hosting.GoFeatureFlag/README.md
new file mode 100644
index 00000000..c7c6fdc0
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.GoFeatureFlag/README.md
@@ -0,0 +1,36 @@
+# CommunityToolkit.Aspire.Hosting.GoFeatureFlag library
+
+Provides extension methods and resource definitions for the .NET Aspire AppHost to support running [GO Feature Flag](https://gofeatureflag.org/) containers.
+
+## Getting Started
+
+### Install the package
+
+In your AppHost project, install the package using the following command:
+
+```dotnetcli
+dotnet add package CommunityToolkit.Aspire.Hosting.GoFeatureFlag
+```
+
+### Example usage
+
+Then, in the _Program.cs_ file of `AppHost`, add a GO Feature Flag resource and consume the connection using the following methods:
+
+```csharp
+var builder = DistributedApplication.CreateBuilder(args);
+
+var goff = builder.AddGoFeatureFlag("goff");
+
+var myService = builder.AddProject()
+ .WithReference(goff);
+
+builder.Build().Run();
+```
+
+## Additional Information
+
+https://learn.microsoft.com/dotnet/aspire/community-toolkit/hosting-go-feature-flag
+
+## Feedback & contributing
+
+https://github.com/CommunityToolkit/Aspire
diff --git a/tests/CommunityToolkit.Aspire.GoFeatureFlag.Tests/AspireGoFeatureFlagClientExtensionsTest.cs b/tests/CommunityToolkit.Aspire.GoFeatureFlag.Tests/AspireGoFeatureFlagClientExtensionsTest.cs
new file mode 100644
index 00000000..4d07a5cf
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.GoFeatureFlag.Tests/AspireGoFeatureFlagClientExtensionsTest.cs
@@ -0,0 +1,139 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Components.Common.Tests;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+using Microsoft.Extensions.Hosting;
+using OpenFeature.Contrib.Providers.GOFeatureFlag;
+
+namespace CommunityToolkit.Aspire.GoFeatureFlag.Tests;
+
+public class AspireGoFeatureFlagClientExtensionsTest(GoFeatureFlagContainerFixture containerFixture) : IClassFixture
+{
+ private const string DefaultConnectionName = "goff";
+
+ private string DefaultConnectionString =>
+ RequiresDockerAttribute.IsSupported ? containerFixture.GetConnectionString() : "http://localhost:27011";
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ [RequiresDocker]
+ public async Task AddGoFeatureFlagClient_HealthCheckShouldBeRegisteredWhenEnabled(bool useKeyed)
+ {
+ var key = DefaultConnectionName;
+
+ var builder = CreateBuilder(DefaultConnectionString);
+
+ if (useKeyed)
+ {
+ builder.AddKeyedGoFeatureFlagClient(key, settings =>
+ {
+ settings.DisableHealthChecks = false;
+ });
+ }
+ else
+ {
+ builder.AddGoFeatureFlagClient(DefaultConnectionName, settings =>
+ {
+ settings.DisableHealthChecks = false;
+ });
+ }
+
+ using var host = builder.Build();
+
+ var healthCheckService = host.Services.GetRequiredService();
+
+ var healthCheckReport = await healthCheckService.CheckHealthAsync();
+
+ var healthCheckName = useKeyed ? $"Goff_{key}" : "Goff";
+
+ Assert.Contains(healthCheckReport.Entries, x => x.Key == healthCheckName);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void AddGoFeatureFlagClient_HealthCheckShouldNotBeRegisteredWhenDisabled(bool useKeyed)
+ {
+ var builder = CreateBuilder(DefaultConnectionString);
+
+ if (useKeyed)
+ {
+ builder.AddKeyedGoFeatureFlagClient(DefaultConnectionName, settings =>
+ {
+ settings.DisableHealthChecks = true;
+ });
+ }
+ else
+ {
+ builder.AddGoFeatureFlagClient(DefaultConnectionName, settings =>
+ {
+ settings.DisableHealthChecks = true;
+ });
+ }
+
+ using var host = builder.Build();
+
+ var healthCheckService = host.Services.GetService();
+
+ Assert.Null(healthCheckService);
+ }
+
+ [Fact]
+ public void CanAddMultipleKeyedServices()
+ {
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+ builder.Configuration.AddInMemoryCollection([
+ new KeyValuePair("ConnectionStrings:goff1", "http://localhost:19530"),
+ new KeyValuePair("ConnectionStrings:goff2", "http://localhost:19531"),
+ new KeyValuePair("ConnectionStrings:goff3", "http://localhost:19532"),
+ ]);
+
+ builder.AddGoFeatureFlagClient("goff1");
+ builder.AddKeyedGoFeatureFlagClient("goff2");
+ builder.AddKeyedGoFeatureFlagClient("goff3");
+
+ using var host = builder.Build();
+
+ var client1 = host.Services.GetRequiredService();
+ var client2 = host.Services.GetRequiredKeyedService("goff2");
+ var client3 = host.Services.GetRequiredKeyedService("goff3");
+
+ Assert.NotSame(client1, client2);
+ Assert.NotSame(client1, client3);
+ Assert.NotSame(client2, client3);
+ }
+
+ [Fact]
+ public void CanAddClientFromEncodedConnectionString()
+ {
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+
+ builder.Configuration.AddInMemoryCollection([
+ new KeyValuePair("ConnectionStrings:goff1", "Endpoint=http://localhost:19530"),
+ new KeyValuePair("ConnectionStrings:goff2", "Endpoint=http://localhost:19531"),
+ ]);
+
+ builder.AddGoFeatureFlagClient("goff1");
+ builder.AddKeyedGoFeatureFlagClient("goff2");
+
+ using var host = builder.Build();
+
+ var client1 = host.Services.GetRequiredService();
+ var client2 = host.Services.GetRequiredKeyedService("goff2");
+
+ Assert.NotSame(client1, client2);
+ }
+
+ private static HostApplicationBuilder CreateBuilder(string connectionString)
+ {
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+
+ builder.Configuration.AddInMemoryCollection([
+ new KeyValuePair($"ConnectionStrings:{DefaultConnectionName}", connectionString)
+ ]);
+ return builder;
+ }
+}
diff --git a/tests/CommunityToolkit.Aspire.GoFeatureFlag.Tests/CommunityToolkit.Aspire.GoFeatureFlag.Tests.csproj b/tests/CommunityToolkit.Aspire.GoFeatureFlag.Tests/CommunityToolkit.Aspire.GoFeatureFlag.Tests.csproj
new file mode 100644
index 00000000..4fb03094
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.GoFeatureFlag.Tests/CommunityToolkit.Aspire.GoFeatureFlag.Tests.csproj
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+
+
diff --git a/tests/CommunityToolkit.Aspire.GoFeatureFlag.Tests/ConfigurationTests.cs b/tests/CommunityToolkit.Aspire.GoFeatureFlag.Tests/ConfigurationTests.cs
new file mode 100644
index 00000000..4c66dc24
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.GoFeatureFlag.Tests/ConfigurationTests.cs
@@ -0,0 +1,15 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace CommunityToolkit.Aspire.GoFeatureFlag.Tests;
+
+public class ConfigurationTests
+{
+ [Fact]
+ public void EndpointIsNullByDefault() =>
+ Assert.Null(new GoFeatureFlagClientSettings().Endpoint);
+
+ [Fact]
+ public void HealthChecksEnabledByDefault() =>
+ Assert.False(new GoFeatureFlagClientSettings().DisableHealthChecks);
+}
diff --git a/tests/CommunityToolkit.Aspire.GoFeatureFlag.Tests/ConformanceTests.cs b/tests/CommunityToolkit.Aspire.GoFeatureFlag.Tests/ConformanceTests.cs
new file mode 100644
index 00000000..e5942de1
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.GoFeatureFlag.Tests/ConformanceTests.cs
@@ -0,0 +1,115 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Components.Common.Tests;
+using Aspire.Components.ConformanceTests;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Hosting;
+using OpenFeature.Contrib.Providers.GOFeatureFlag;
+using OpenFeature.Model;
+
+namespace CommunityToolkit.Aspire.GoFeatureFlag.Tests;
+
+public class ConformanceTests : ConformanceTests, IClassFixture
+{
+ private readonly GoFeatureFlagContainerFixture _containerFixture;
+
+ protected override ServiceLifetime ServiceLifetime => ServiceLifetime.Singleton;
+
+ protected override string ActivitySourceName => string.Empty;
+
+ protected override string[] RequiredLogCategories => [];
+
+ protected override bool CanConnectToServer => RequiresDockerAttribute.IsSupported;
+
+ protected override bool SupportsKeyedRegistrations => true;
+
+ public ConformanceTests(GoFeatureFlagContainerFixture containerFixture)
+ {
+ _containerFixture = containerFixture;
+ }
+
+ protected override void PopulateConfiguration(ConfigurationManager configuration, string? key = null)
+ {
+ var connectionString = RequiresDockerAttribute.IsSupported ?
+ $"{_containerFixture.GetConnectionString()}" :
+ "Endpoint=http://localhost:27017";
+
+ configuration.AddInMemoryCollection(
+ [
+ new KeyValuePair(CreateConfigKey("Aspire:GoFeatureFlag:Client", key, "Endpoint"), GetConnectionStringKeyValue(connectionString,"Endpoint")),
+ new KeyValuePair($"ConnectionStrings:{key ?? "goff"}", $"{connectionString}")
+ ]);
+ }
+
+ internal static string GetConnectionStringKeyValue(string connectionString, string configKey)
+ {
+ // from the connection string, extract the key value of the configKey
+ var parts = connectionString.Split(';');
+ foreach (var part in parts)
+ {
+ var keyValue = part.Split('=');
+ if (keyValue.Length == 2 && keyValue[0].Equals(configKey, StringComparison.OrdinalIgnoreCase))
+ {
+ return keyValue[1];
+ }
+ }
+ return string.Empty;
+ }
+
+ protected override void RegisterComponent(HostApplicationBuilder builder, Action? configure = null, string? key = null)
+ {
+ if (key is null)
+ {
+ builder.AddGoFeatureFlagClient("goff", configureSettings: configure);
+ }
+ else
+ {
+ builder.AddKeyedGoFeatureFlagClient(key, configureSettings: configure);
+ }
+ }
+
+ protected override string ValidJsonConfig => """
+ {
+ "Aspire": {
+ "GoFeatureFlag": {
+ "Client": {
+ "Endpoint": "http://localhost:19530"
+ }
+ }
+ }
+ }
+ """;
+
+ protected override (string json, string error)[] InvalidJsonToErrorMessage => new[]
+ {
+ ("""{"Aspire": { "GoFeatureFlag":{ "Client": { "Endpoint": 3 }}}}""", "Value is \"integer\" but should be \"string\""),
+ ("""{"Aspire": { "GoFeatureFlag":{ "Client": { "Endpoint": "hello" }}}}""", "Value does not match format \"uri\"")
+ };
+
+ protected override void SetHealthCheck(GoFeatureFlagClientSettings options, bool enabled)
+ {
+ options.DisableHealthChecks = !enabled;
+ }
+
+ protected override void SetMetrics(GoFeatureFlagClientSettings options, bool enabled)
+ {
+ throw new NotImplementedException();
+ }
+
+ protected override void SetTracing(GoFeatureFlagClientSettings options, bool enabled)
+ {
+ throw new NotImplementedException();
+ }
+
+ protected override void TriggerActivity(GoFeatureFlagProvider service)
+ {
+ using var source = new CancellationTokenSource(100);
+
+ var context = EvaluationContext.Builder()
+ .Set("targetingKey", Guid.NewGuid().ToString())
+ .Set("anonymous", true)
+ .Build();
+ service.InitializeAsync(context, source.Token).Wait();
+ }
+}
diff --git a/tests/CommunityToolkit.Aspire.GoFeatureFlag.Tests/GoFeatureFlagClientPublicApiTests.cs b/tests/CommunityToolkit.Aspire.GoFeatureFlag.Tests/GoFeatureFlagClientPublicApiTests.cs
new file mode 100644
index 00000000..8ded4145
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.GoFeatureFlag.Tests/GoFeatureFlagClientPublicApiTests.cs
@@ -0,0 +1,87 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.Extensions.Hosting;
+
+namespace CommunityToolkit.Aspire.GoFeatureFlag.Tests;
+
+public class GoFeatureFlagClientPublicApiTests
+{
+ [Fact]
+ public void AddGoFeatureFlagClientShouldThrowWhenBuilderIsNull()
+ {
+ IHostApplicationBuilder builder = null!;
+
+ var connectionName = "goff";
+
+ var action = () => builder.AddGoFeatureFlagClient(connectionName);
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(builder), exception.ParamName);
+ }
+
+ [Fact]
+ public void AddGoFeatureFlagClientShouldThrowWhenNameIsNull()
+ {
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+
+ string connectionName = null!;
+
+ var action = () => builder.AddGoFeatureFlagClient(connectionName);
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(connectionName), exception.ParamName);
+ }
+
+ [Fact]
+ public void AddGoFeatureFlagClientShouldThrowWhenNameIsEmpty()
+ {
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+
+ string connectionName = "";
+
+ var action = () => builder.AddGoFeatureFlagClient(connectionName);
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(connectionName), exception.ParamName);
+ }
+
+ [Fact]
+ public void AddKeyedGoFeatureFlagClientShouldThrowWhenBuilderIsNull()
+ {
+ IHostApplicationBuilder builder = null!;
+
+ var connectionName = "goff";
+
+ var action = () => builder.AddKeyedGoFeatureFlagClient(connectionName);
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(builder), exception.ParamName);
+ }
+
+ [Fact]
+ public void AddKeyedGoFeatureFlagClientShouldThrowWhenNameIsNull()
+ {
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+
+ string name = null!;
+
+ var action = () => builder.AddKeyedGoFeatureFlagClient(name);
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(name), exception.ParamName);
+ }
+
+ [Fact]
+ public void AddKeyedGoFeatureFlagClientShouldThrowWhenNameIsEmpty()
+ {
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+
+ string name = "";
+
+ var action = () => builder.AddKeyedGoFeatureFlagClient(name);
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(name), exception.ParamName);
+ }
+}
diff --git a/tests/CommunityToolkit.Aspire.GoFeatureFlag.Tests/GoFeatureFlagContainerFixture.cs b/tests/CommunityToolkit.Aspire.GoFeatureFlag.Tests/GoFeatureFlagContainerFixture.cs
new file mode 100644
index 00000000..591ece83
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.GoFeatureFlag.Tests/GoFeatureFlagContainerFixture.cs
@@ -0,0 +1,48 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using CommunityToolkit.Aspire.Hosting.GoFeatureFlag;
+using Aspire.Components.Common.Tests;
+using Aspire.Hosting;
+using DotNet.Testcontainers.Builders;
+using DotNet.Testcontainers.Containers;
+
+namespace CommunityToolkit.Aspire.GoFeatureFlag.Tests;
+
+public sealed class GoFeatureFlagContainerFixture : IAsyncLifetime
+{
+ public IContainer? Container { get; private set; }
+ public string GetConnectionString()
+ {
+ if (Container is null)
+ {
+ throw new InvalidOperationException("The test container was not initialized.");
+ }
+ var endpoint = new UriBuilder("http", Container.Hostname, Container.GetMappedPublicPort(1031)).ToString();
+ return $"Endpoint={endpoint}";
+ }
+
+ public async Task InitializeAsync()
+ {
+ if (RequiresDockerAttribute.IsSupported)
+ {
+ var source = Path.GetFullPath("./goff", Directory.GetCurrentDirectory());
+ Container = new ContainerBuilder()
+ .WithImage($"{GoFeatureFlagContainerImageTags.Registry}/{GoFeatureFlagContainerImageTags.Image}:{GoFeatureFlagContainerImageTags.Tag}")
+ .WithPortBinding(1031, true)
+ .WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(r => r.ForPath("/health").ForPort(1031)))
+ .WithBindMount(source, "/goff")
+ .Build();
+
+ await Container.StartAsync();
+ }
+ }
+
+ public async Task DisposeAsync()
+ {
+ if (Container is not null)
+ {
+ await Container.DisposeAsync();
+ }
+ }
+}
diff --git a/tests/CommunityToolkit.Aspire.GoFeatureFlag.Tests/goff/flags.yaml b/tests/CommunityToolkit.Aspire.GoFeatureFlag.Tests/goff/flags.yaml
new file mode 100644
index 00000000..b3efd1f2
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.GoFeatureFlag.Tests/goff/flags.yaml
@@ -0,0 +1,6 @@
+display-banner:
+ variations:
+ enabled: true
+ disabled: false
+ defaultRule:
+ variation: enabled
diff --git a/tests/CommunityToolkit.Aspire.GoFeatureFlag.Tests/goff/goff-proxy.yaml b/tests/CommunityToolkit.Aspire.GoFeatureFlag.Tests/goff/goff-proxy.yaml
new file mode 100644
index 00000000..4a373a32
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.GoFeatureFlag.Tests/goff/goff-proxy.yaml
@@ -0,0 +1,3 @@
+retrievers:
+ - kind: file
+ path: /goff/flags.yaml
diff --git a/tests/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.Tests/AddGoFeatureFlagTests.cs b/tests/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.Tests/AddGoFeatureFlagTests.cs
new file mode 100644
index 00000000..c2ff27e2
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.Tests/AddGoFeatureFlagTests.cs
@@ -0,0 +1,92 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Net.Sockets;
+using Aspire.Hosting;
+
+namespace CommunityToolkit.Aspire.Hosting.GoFeatureFlag.Tests;
+
+public class AddGoFeatureFlagTests
+{
+ [Fact]
+ public void AddGoFeatureFlagContainerWithDefaultsAddsAnnotationMetadata()
+ {
+ var appBuilder = DistributedApplication.CreateBuilder();
+
+ var goff = appBuilder.AddGoFeatureFlag("goff");
+
+ using var app = appBuilder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+
+ var containerResource = Assert.Single(appModel.Resources.OfType());
+ Assert.Equal("goff", containerResource.Name);
+
+ var endpoints = containerResource.Annotations.OfType();
+ Assert.Single(endpoints);
+
+ var primaryEndpoint = Assert.Single(endpoints, e => e.Name == "http");
+ Assert.Equal(1031, primaryEndpoint.TargetPort);
+ Assert.False(primaryEndpoint.IsExternal);
+ Assert.Equal("http", primaryEndpoint.Name);
+ Assert.Null(primaryEndpoint.Port);
+ Assert.Equal(ProtocolType.Tcp, primaryEndpoint.Protocol);
+ Assert.Equal("http", primaryEndpoint.Transport);
+ Assert.Equal("http", primaryEndpoint.UriScheme);
+
+ var containerAnnotation = Assert.Single(containerResource.Annotations.OfType());
+ Assert.Equal(GoFeatureFlagContainerImageTags.Tag, containerAnnotation.Tag);
+ Assert.Equal(GoFeatureFlagContainerImageTags.Image, containerAnnotation.Image);
+ Assert.Equal(GoFeatureFlagContainerImageTags.Registry, containerAnnotation.Registry);
+ }
+
+ [Fact]
+ public void AddGoFeatureFlagContainerAddsAnnotationMetadata()
+ {
+ var appBuilder = DistributedApplication.CreateBuilder();
+ var goff = appBuilder.AddGoFeatureFlag("goff");
+
+ using var app = appBuilder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+
+ var containerResource = Assert.Single(appModel.Resources.OfType());
+ Assert.Equal("goff", containerResource.Name);
+
+ var endpoints = containerResource.Annotations.OfType();
+ Assert.Single(endpoints);
+
+ var primaryEndpoint = Assert.Single(endpoints, e => e.Name == "http");
+ Assert.Equal(1031, primaryEndpoint.TargetPort);
+ Assert.False(primaryEndpoint.IsExternal);
+ Assert.Equal("http", primaryEndpoint.Name);
+ Assert.Null(primaryEndpoint.Port);
+ Assert.Equal(ProtocolType.Tcp, primaryEndpoint.Protocol);
+ Assert.Equal("http", primaryEndpoint.Transport);
+ Assert.Equal("http", primaryEndpoint.UriScheme);
+
+ var containerAnnotation = Assert.Single(containerResource.Annotations.OfType());
+ Assert.Equal(GoFeatureFlagContainerImageTags.Tag, containerAnnotation.Tag);
+ Assert.Equal(GoFeatureFlagContainerImageTags.Image, containerAnnotation.Image);
+ Assert.Equal(GoFeatureFlagContainerImageTags.Registry, containerAnnotation.Registry);
+ }
+
+ [Fact]
+ public async Task GoFeatureFlagCreatesConnectionString()
+ {
+ var appBuilder = DistributedApplication.CreateBuilder();
+ var goff = appBuilder
+ .AddGoFeatureFlag("goff")
+ .WithEndpoint("http", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 27020));
+
+ using var app = appBuilder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+
+ var connectionStringResource = Assert.Single(appModel.Resources.OfType()) as IResourceWithConnectionString;
+ var connectionString = await connectionStringResource.GetConnectionStringAsync();
+
+ Assert.Equal($"Endpoint=http://localhost:27020", connectionString);
+ Assert.Equal("Endpoint=http://{goff.bindings.http.host}:{goff.bindings.http.port}", connectionStringResource.ConnectionStringExpression.ValueExpression);
+ }
+}
diff --git a/tests/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.Tests/AppHostTests.cs b/tests/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.Tests/AppHostTests.cs
new file mode 100644
index 00000000..35820681
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.Tests/AppHostTests.cs
@@ -0,0 +1,39 @@
+using CommunityToolkit.Aspire.Testing;
+using Aspire.Components.Common.Tests;
+using OpenFeature.Model;
+using System.Net.Http.Json;
+
+namespace CommunityToolkit.Aspire.Hosting.GoFeatureFlag.Tests;
+
+[RequiresDocker]
+public class AppHostTests(AspireIntegrationTestFixture fixture) : IClassFixture>
+{
+ [Fact]
+ public async Task ResourceStartsAndRespondsOk()
+ {
+ var resourceName = "goff";
+ await fixture.ResourceNotificationService.WaitForResourceHealthyAsync(resourceName).WaitAsync(TimeSpan.FromMinutes(5));
+ var httpClient = fixture.CreateHttpClient(resourceName);
+
+ var response = await httpClient.GetAsync("/info");
+
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task ApiServiceRetrieveFlags()
+ {
+ var resourceName = "apiservice";
+
+ await fixture.ResourceNotificationService.WaitForResourceHealthyAsync("goff").WaitAsync(TimeSpan.FromMinutes(5));
+ await fixture.ResourceNotificationService.WaitForResourceHealthyAsync(resourceName).WaitAsync(TimeSpan.FromMinutes(5));
+ var httpClient = fixture.CreateHttpClient(resourceName);
+
+ var getResponse = await httpClient.GetAsync("/features/display-banner");
+ Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode);
+
+ var data = await getResponse.Content.ReadFromJsonAsync>();
+ Assert.NotNull(data);
+ Assert.True(data.Value);
+ }
+}
\ No newline at end of file
diff --git a/tests/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.Tests/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.Tests.csproj b/tests/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.Tests/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.Tests.csproj
new file mode 100644
index 00000000..7277d71e
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.Tests/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.Tests.csproj
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+
+
diff --git a/tests/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.Tests/GoFeatureFlagFunctionalTests.cs b/tests/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.Tests/GoFeatureFlagFunctionalTests.cs
new file mode 100644
index 00000000..24e5d9d0
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.Tests/GoFeatureFlagFunctionalTests.cs
@@ -0,0 +1,107 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Components.Common.Tests;
+using Aspire.Hosting;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+using Microsoft.Extensions.Hosting;
+using Xunit.Abstractions;
+using Aspire.Hosting.Utils;
+using CommunityToolkit.Aspire.Testing;
+using OpenFeature.Contrib.Providers.GOFeatureFlag;
+using OpenFeature.Model;
+
+namespace CommunityToolkit.Aspire.Hosting.GoFeatureFlag.Tests;
+
+[RequiresDocker]
+public class GoFeatureFlagFunctionalTests(ITestOutputHelper testOutputHelper)
+{
+ private static readonly string SOURCE = Path.GetFullPath("./goff", Directory.GetCurrentDirectory());
+
+ [Fact]
+ public async Task VerifyGoFeatureFlagResource()
+ {
+ using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
+
+ var goff = builder.AddGoFeatureFlag("goff")
+ .WithBindMount(SOURCE, "/goff");
+
+ using var app = builder.Build();
+
+ await app.StartAsync();
+
+ var rns = app.Services.GetRequiredService();
+
+ await rns.WaitForResourceHealthyAsync(goff.Resource.Name);
+
+ var hb = Host.CreateApplicationBuilder();
+
+ hb.Configuration[$"ConnectionStrings:{goff.Resource.Name}"] = await goff.Resource.ConnectionStringExpression.GetValueAsync(default);
+
+ hb.AddGoFeatureFlagClient(goff.Resource.Name);
+
+ using var host = hb.Build();
+
+ await host.StartAsync();
+
+ var goFeatureFlagProvider = host.Services.GetRequiredService();
+
+ await VerifyTestData(goFeatureFlagProvider);
+ }
+
+ [Fact]
+ public async Task VerifyWaitForOnGoFeatureFlagBlocksDependentResources()
+ {
+ var cts = new CancellationTokenSource(TimeSpan.FromMinutes(10));
+ using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
+
+ var healthCheckTcs = new TaskCompletionSource();
+ builder.Services.AddHealthChecks().AddAsyncCheck("blocking_check", () =>
+ {
+ return healthCheckTcs.Task;
+ });
+
+ var resource = builder.AddGoFeatureFlag("resource")
+ .WithBindMount(SOURCE, "/goff")
+ .WithHealthCheck("blocking_check");
+
+ var dependentResource = builder.AddGoFeatureFlag("dependentresource")
+ .WithBindMount(SOURCE, "/goff")
+ .WaitFor(resource);
+
+ using var app = builder.Build();
+
+ var pendingStart = app.StartAsync(cts.Token);
+
+ var rns = app.Services.GetRequiredService();
+
+ await rns.WaitForResourceAsync(resource.Resource.Name, KnownResourceStates.Running, cts.Token);
+
+ await rns.WaitForResourceAsync(dependentResource.Resource.Name, KnownResourceStates.Waiting, cts.Token);
+
+ healthCheckTcs.SetResult(HealthCheckResult.Healthy());
+
+ await rns.WaitForResourceAsync(resource.Resource.Name, re => re.Snapshot.HealthStatus == HealthStatus.Healthy, cts.Token);
+
+ await rns.WaitForResourceAsync(dependentResource.Resource.Name, KnownResourceStates.Running, cts.Token);
+
+ await pendingStart;
+
+ await app.StopAsync();
+ }
+
+ private static async Task VerifyTestData(GoFeatureFlagProvider goFeatureFlagProvider)
+ {
+ var userContext = EvaluationContext.Builder()
+ .Set("targetingKey", Guid.NewGuid().ToString())
+ .Set("anonymous", true)
+ .Build();
+ string featureName = "display-banner";
+ var flag = await goFeatureFlagProvider.ResolveBooleanValueAsync(
+ featureName, false, userContext
+ );
+
+ Assert.NotNull(flag);
+ Assert.True(flag.Value);
+ }
+}
\ No newline at end of file
diff --git a/tests/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.Tests/GoFeatureFlagPublicApiTests.cs b/tests/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.Tests/GoFeatureFlagPublicApiTests.cs
new file mode 100644
index 00000000..b73a932f
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.Tests/GoFeatureFlagPublicApiTests.cs
@@ -0,0 +1,82 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Hosting;
+
+namespace CommunityToolkit.Aspire.Hosting.GoFeatureFlag.Tests;
+
+public class GoFeatureFlagPublicApiTests
+{
+ [Fact]
+ public void AddGoFeatureFlagContainerShouldThrowWhenBuilderIsNull()
+ {
+ IDistributedApplicationBuilder builder = null!;
+ const string name = "Goff";
+
+ var action = () => builder.AddGoFeatureFlag(name);
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(builder), exception.ParamName);
+ }
+
+ [Fact]
+ public void AddGoFeatureFlagContainerShouldThrowWhenNameIsNull()
+ {
+ IDistributedApplicationBuilder builder = new DistributedApplicationBuilder([]);
+ string name = null!;
+
+ var action = () => builder.AddGoFeatureFlag(name);
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(name), exception.ParamName);
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void WithGoffBindMountShouldThrowWhenBuilderIsNull(bool useVolume)
+ {
+ IResourceBuilder builder = null!;
+
+ Func>? action = null;
+
+ if (useVolume)
+ {
+ action = () => builder.WithDataVolume();
+ }
+ else
+ {
+ const string source = "/data";
+
+ action = () => builder.WithGoffBindMount(source);
+ }
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(builder), exception.ParamName);
+ }
+
+ [Fact]
+ public void WithGoffBindMountShouldThrowWhenSourceIsNull()
+ {
+ var builder = new DistributedApplicationBuilder([]);
+ var resourceBuilder = builder.AddGoFeatureFlag("Goff");
+
+ string source = null!;
+
+ var action = () => resourceBuilder.WithGoffBindMount(source);
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(source), exception.ParamName);
+ }
+
+ [Fact]
+ public void CtorGoFeatureFlagResourceShouldThrowWhenNameIsNull()
+ {
+ const string name = null!;
+
+ var action = () => new GoFeatureFlagResource(name!);
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(name), exception.ParamName);
+ }
+}
diff --git a/tests/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.Tests/goff/flags.yaml b/tests/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.Tests/goff/flags.yaml
new file mode 100644
index 00000000..b3efd1f2
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.Tests/goff/flags.yaml
@@ -0,0 +1,6 @@
+display-banner:
+ variations:
+ enabled: true
+ disabled: false
+ defaultRule:
+ variation: enabled
diff --git a/tests/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.Tests/goff/goff-proxy.yaml b/tests/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.Tests/goff/goff-proxy.yaml
new file mode 100644
index 00000000..4a373a32
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.Tests/goff/goff-proxy.yaml
@@ -0,0 +1,3 @@
+retrievers:
+ - kind: file
+ path: /goff/flags.yaml