diff --git a/.github/workflows/pr-test.yml b/.github/workflows/pr-test.yml
index 15098d9..050257d 100644
--- a/.github/workflows/pr-test.yml
+++ b/.github/workflows/pr-test.yml
@@ -43,4 +43,21 @@ jobs:
echo "::error file=Makefile::Doc generation produced diff. Run 'make generate-docs' and commit results."
git diff
exit 1
- fi
\ No newline at end of file
+ fi
+
+ csharp-test:
+ name: 'C# Generator Test'
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version-file: 'go.mod'
+
+ - name: Set up Docker
+ uses: docker/setup-buildx-action@v2
+
+ - name: 'Run C# integration test'
+ run: ./test/csharp-integration/test-compilation.sh
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 7f64d84..4f0c385 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -32,6 +32,23 @@ We welcome contributions for new generators to extend the functionality of the O
11. **Address Feedback**: Be responsive to feedback from the maintainers. Make any necessary changes and update your pull request as needed.
+### Integration Tests
+
+To verify that generated code compiles correctly, the project includes integration tests, for example, for c#:
+
+```bash
+# Test the C# generator output
+make test-csharp
+```
+
+This will:
+1. Build the CLI
+2. Generate a C# client
+3. Compile the C# code in a Docker container
+4. Validate that the code compiles correctly
+
+Consider adding more integration tests for new generators.
+
## Templates
### Data
diff --git a/Makefile b/Makefile
index 010c97e..5e87ad0 100644
--- a/Makefile
+++ b/Makefile
@@ -5,6 +5,11 @@ test:
@go test -v ./...
@echo "Tests passed successfully!"
+.PHONY: test-csharp
+test-csharp:
+ @echo "Running C# integration test..."
+ @./test/csharp-integration/test-compilation.sh
+
generate-docs:
@echo "Generating documentation..."
@go run ./docs/generate-commands.go
diff --git a/cmd/config.go b/cmd/config.go
index 54dc44b..c2665c3 100644
--- a/cmd/config.go
+++ b/cmd/config.go
@@ -18,7 +18,7 @@ func initializeConfig(cmd *cobra.Command, bindPrefix string) error {
// Set the config file name and path
v.SetConfigName(".openfeature")
v.AddConfigPath(".")
-
+
logger.Default.Debug("Looking for .openfeature config file in current directory")
// Read the config file
@@ -31,7 +31,6 @@ func initializeConfig(cmd *cobra.Command, bindPrefix string) error {
} else {
logger.Default.Debug(fmt.Sprintf("Using config file: %s", v.ConfigFileUsed()))
}
-
// Track which flags were set directly via command line
cmdLineFlags := make(map[string]bool)
@@ -50,11 +49,11 @@ func initializeConfig(cmd *cobra.Command, bindPrefix string) error {
// Build configuration paths from most specific to least specific
configPaths := []string{}
-
+
// Check the most specific path (e.g., generate.go.package-name)
if bindPrefix != "" {
- configPaths = append(configPaths, bindPrefix + "." + f.Name)
-
+ configPaths = append(configPaths, bindPrefix+"."+f.Name)
+
// Check parent paths (e.g., generate.package-name)
parts := strings.Split(bindPrefix, ".")
for i := len(parts) - 1; i > 0; i-- {
@@ -62,12 +61,12 @@ func initializeConfig(cmd *cobra.Command, bindPrefix string) error {
configPaths = append(configPaths, parentPath)
}
}
-
+
// Check the base path (e.g., package-name)
configPaths = append(configPaths, f.Name)
-
+
logger.Default.Debug(fmt.Sprintf("Looking for config value for flag %s in paths: %s", f.Name, strings.Join(configPaths, ", ")))
-
+
// Try each path in order until we find a match
for _, path := range configPaths {
if v.IsSet(path) {
@@ -81,7 +80,7 @@ func initializeConfig(cmd *cobra.Command, bindPrefix string) error {
}
}
}
-
+
// Log the final value for the flag
logger.Default.Debug(fmt.Sprintf("Final flag value: %s=%s", f.Name, f.Value.String()))
})
diff --git a/cmd/generate.go b/cmd/generate.go
index 8a8b600..d89f9b4 100644
--- a/cmd/generate.go
+++ b/cmd/generate.go
@@ -6,6 +6,7 @@ import (
"github.com/open-feature/cli/internal/config"
"github.com/open-feature/cli/internal/flagset"
"github.com/open-feature/cli/internal/generators"
+ "github.com/open-feature/cli/internal/generators/csharp"
"github.com/open-feature/cli/internal/generators/golang"
"github.com/open-feature/cli/internal/generators/nodejs"
"github.com/open-feature/cli/internal/generators/python"
@@ -14,6 +15,32 @@ import (
"github.com/spf13/cobra"
)
+func GetGenerateCmd() *cobra.Command {
+ generateCmd := &cobra.Command{
+ Use: "generate",
+ Short: "Generate typesafe OpenFeature accessors.",
+ PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
+ return initializeConfig(cmd, "generate")
+ },
+ RunE: func(cmd *cobra.Command, args []string) error {
+ cmd.Println("Available generators:")
+ return generators.DefaultManager.PrintGeneratorsTable()
+ },
+ }
+
+ // Add generate flags using the config package
+ config.AddGenerateFlags(generateCmd)
+
+ // Add all registered generator commands
+ for _, subCmd := range generators.DefaultManager.GetCommands() {
+ generateCmd.AddCommand(subCmd)
+ }
+
+ addStabilityInfo(generateCmd)
+
+ return generateCmd
+}
+
// addStabilityInfo adds stability information to the command's help template before "Usage:"
func addStabilityInfo(cmd *cobra.Command) {
// Only modify commands that have a stability annotation
@@ -37,7 +64,7 @@ func addStabilityInfo(cmd *cobra.Command) {
}
}
-func GetGenerateNodeJSCmd() *cobra.Command {
+func getGenerateNodeJSCmd() *cobra.Command {
nodeJSCmd := &cobra.Command{
Use: "nodejs",
Short: "Generate typesafe Node.js client.",
@@ -69,9 +96,9 @@ func GetGenerateNodeJSCmd() *cobra.Command {
if err != nil {
return err
}
-
+
logger.Default.GenerationComplete("Node.js")
-
+
return nil
},
}
@@ -81,7 +108,7 @@ func GetGenerateNodeJSCmd() *cobra.Command {
return nodeJSCmd
}
-func GetGenerateReactCmd() *cobra.Command {
+func getGenerateReactCmd() *cobra.Command {
reactCmd := &cobra.Command{
Use: "react",
Short: "Generate typesafe React Hooks.",
@@ -95,7 +122,7 @@ func GetGenerateReactCmd() *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error {
manifestPath := config.GetManifestPath(cmd)
outputPath := config.GetOutputPath(cmd)
-
+
logger.Default.GenerationStarted("React")
params := generators.Params[react.Params]{
@@ -113,9 +140,9 @@ func GetGenerateReactCmd() *cobra.Command {
if err != nil {
return err
}
-
+
logger.Default.GenerationComplete("React")
-
+
return nil
},
}
@@ -125,7 +152,57 @@ func GetGenerateReactCmd() *cobra.Command {
return reactCmd
}
-func GetGenerateGoCmd() *cobra.Command {
+func getGenerateCSharpCmd() *cobra.Command {
+ csharpCmd := &cobra.Command{
+ Use: "csharp",
+ Short: "Generate typesafe C# client.",
+ Long: `Generate typesafe C# client compatible with the OpenFeature .NET SDK.`,
+ Annotations: map[string]string{
+ "stability": string(generators.Alpha),
+ },
+ PreRunE: func(cmd *cobra.Command, args []string) error {
+ return initializeConfig(cmd, "generate.csharp")
+ },
+ RunE: func(cmd *cobra.Command, args []string) error {
+ namespace := config.GetCSharpNamespace(cmd)
+ manifestPath := config.GetManifestPath(cmd)
+ outputPath := config.GetOutputPath(cmd)
+
+ logger.Default.GenerationStarted("C#")
+
+ params := generators.Params[csharp.Params]{
+ OutputPath: outputPath,
+ Custom: csharp.Params{
+ Namespace: namespace,
+ },
+ }
+ flagset, err := flagset.Load(manifestPath)
+ if err != nil {
+ return err
+ }
+
+ generator := csharp.NewGenerator(flagset)
+ logger.Default.Debug("Executing C# generator")
+ err = generator.Generate(¶ms)
+ if err != nil {
+ return err
+ }
+
+ logger.Default.GenerationComplete("C#")
+
+ return nil
+ },
+ }
+
+ // Add C#-specific flags
+ config.AddCSharpGenerateFlags(csharpCmd)
+
+ addStabilityInfo(csharpCmd)
+
+ return csharpCmd
+}
+
+func getGenerateGoCmd() *cobra.Command {
goCmd := &cobra.Command{
Use: "go",
Short: "Generate typesafe accessors for OpenFeature.",
@@ -140,7 +217,7 @@ func GetGenerateGoCmd() *cobra.Command {
goPackageName := config.GetGoPackageName(cmd)
manifestPath := config.GetManifestPath(cmd)
outputPath := config.GetOutputPath(cmd)
-
+
logger.Default.GenerationStarted("Go")
params := generators.Params[golang.Params]{
@@ -161,9 +238,9 @@ func GetGenerateGoCmd() *cobra.Command {
if err != nil {
return err
}
-
+
logger.Default.GenerationComplete("Go")
-
+
return nil
},
}
@@ -219,34 +296,9 @@ func getGeneratePythonCmd() *cobra.Command {
func init() {
// Register generators with the manager
- generators.DefaultManager.Register(GetGenerateReactCmd)
- generators.DefaultManager.Register(GetGenerateGoCmd)
- generators.DefaultManager.Register(GetGenerateNodeJSCmd)
+ generators.DefaultManager.Register(getGenerateReactCmd)
+ generators.DefaultManager.Register(getGenerateGoCmd)
+ generators.DefaultManager.Register(getGenerateNodeJSCmd)
generators.DefaultManager.Register(getGeneratePythonCmd)
-}
-
-func GetGenerateCmd() *cobra.Command {
- generateCmd := &cobra.Command{
- Use: "generate",
- Short: "Generate typesafe OpenFeature accessors.",
- PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
- return initializeConfig(cmd, "generate")
- },
- RunE: func(cmd *cobra.Command, args []string) error {
- cmd.Println("Available generators:")
- return generators.DefaultManager.PrintGeneratorsTable()
- },
- }
-
- // Add generate flags using the config package
- config.AddGenerateFlags(generateCmd)
-
- // Add all registered generator commands
- for _, subCmd := range generators.DefaultManager.GetCommands() {
- generateCmd.AddCommand(subCmd)
- }
-
- addStabilityInfo(generateCmd)
-
- return generateCmd
+ generators.DefaultManager.Register(getGenerateCSharpCmd)
}
diff --git a/cmd/generate_test.go b/cmd/generate_test.go
index b7db5d1..0bc8466 100644
--- a/cmd/generate_test.go
+++ b/cmd/generate_test.go
@@ -21,7 +21,7 @@ type generateTestCase struct {
outputGolden string // path to the golden output file
outputPath string // output directory (optional, defaults to "output")
outputFile string // output file name
- packageName string // optional, only used for Go
+ packageName string // optional, used for Go (package-name) and C# (namespace)
}
func TestGenerate(t *testing.T) {
@@ -55,6 +55,14 @@ func TestGenerate(t *testing.T) {
outputGolden: "testdata/success_python.golden",
outputFile: "openfeature.py",
},
+ {
+ name: "CSharp generation success",
+ command: "csharp",
+ manifestGolden: "testdata/success_manifest.golden",
+ outputGolden: "testdata/success_csharp.golden",
+ outputFile: "OpenFeature.g.cs",
+ packageName: "TestNamespace", // Using packageName field for namespace
+ },
// Add more test cases here as needed
}
@@ -67,7 +75,7 @@ func TestGenerate(t *testing.T) {
// Constant paths
const memoryManifestPath = "manifest/path.json"
-
+
// Use default output path if not specified
outputPath := tc.outputPath
if outputPath == "" {
@@ -86,9 +94,13 @@ func TestGenerate(t *testing.T) {
"--output", outputPath,
}
- // Add package name if provided (for Go)
+ // Add parameters specific to each generator
if tc.packageName != "" {
- args = append(args, "--package-name", tc.packageName)
+ if tc.command == "csharp" {
+ args = append(args, "--namespace", tc.packageName)
+ } else if tc.command == "go" {
+ args = append(args, "--package-name", tc.packageName)
+ }
}
cmd.SetArgs(args)
@@ -107,7 +119,7 @@ func TestGenerate(t *testing.T) {
func readOsFileAndWriteToMemMap(t *testing.T, inputPath string, memPath string, memFs afero.Fs) {
data, err := os.ReadFile(inputPath)
- if (err != nil) {
+ if err != nil {
t.Fatalf("error reading file %q: %v", inputPath, err)
}
if err := memFs.MkdirAll(filepath.Dir(memPath), os.ModePerm); err != nil {
@@ -137,11 +149,11 @@ func compareOutput(t *testing.T, testFile, memoryOutputPath string, fs afero.Fs)
if err != nil {
t.Fatalf("error reading file %q: %v", memoryOutputPath, err)
}
-
+
// Convert to string arrays by splitting on newlines
wantLines := strings.Split(string(want), "\n")
gotLines := strings.Split(string(got), "\n")
-
+
if diff := cmp.Diff(wantLines, gotLines); diff != "" {
t.Errorf("output mismatch (-want +got):\n%s", diff)
}
diff --git a/cmd/init.go b/cmd/init.go
index 9ca5bf5..d650940 100644
--- a/cmd/init.go
+++ b/cmd/init.go
@@ -34,7 +34,7 @@ func GetInitCmd() *cobra.Command {
logger.Default.Info("No changes were made.")
return nil
}
-
+
logger.Default.Debug("User confirmed override of existing manifest")
}
@@ -44,7 +44,7 @@ func GetInitCmd() *cobra.Command {
logger.Default.Error(fmt.Sprintf("Failed to create manifest: %v", err))
return err
}
-
+
logger.Default.FileCreated(manifestPath)
logger.Default.Success("Project initialized.")
return nil
diff --git a/cmd/root.go b/cmd/root.go
index 993145b..4c9c3e4 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -44,7 +44,7 @@ func GetRootCmd() *cobra.Command {
},
RunE: func(cmd *cobra.Command, args []string) error {
printBanner()
- logger.Default.Println("");
+ logger.Default.Println("")
logger.Default.Println("To see all the options, try 'openfeature --help'")
return nil
},
diff --git a/cmd/testdata/success_csharp.golden b/cmd/testdata/success_csharp.golden
new file mode 100644
index 0000000..28b6de1
--- /dev/null
+++ b/cmd/testdata/success_csharp.golden
@@ -0,0 +1,219 @@
+// AUTOMATICALLY GENERATED BY OPENFEATURE CLI, DO NOT EDIT.
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using System.Threading;
+using Microsoft.Extensions.DependencyInjection;
+using OpenFeature;
+using OpenFeature.Model;
+
+namespace TestNamespace
+{
+ ///
+ /// Service collection extensions for OpenFeature
+ ///
+ public static class OpenFeatureServiceExtensions
+ {
+ ///
+ /// Adds OpenFeature services to the service collection with the generated client
+ ///
+ /// The service collection to add services to
+ /// The service collection for chaining
+ public static IServiceCollection AddOpenFeature(this IServiceCollection services)
+ {
+ return services
+ .AddSingleton(_ => Api.Instance)
+ .AddSingleton(provider => provider.GetRequiredService().GetClient())
+ .AddSingleton();
+ }
+
+ ///
+ /// Adds OpenFeature services to the service collection with the generated client for a specific domain
+ ///
+ /// The service collection to add services to
+ /// The domain to get the client for
+ /// The service collection for chaining
+ public static IServiceCollection AddOpenFeature(this IServiceCollection services, string domain)
+ {
+ return services
+ .AddSingleton(_ => Api.Instance)
+ .AddSingleton(provider => provider.GetRequiredService().GetClient(domain))
+ .AddSingleton();
+ }
+ }
+
+ ///
+ /// Generated OpenFeature client for typesafe flag access
+ ///
+ public class GeneratedClient
+ {
+ private readonly IFeatureClient _client;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The OpenFeature client to use for flag evaluations.
+ public GeneratedClient(IFeatureClient client)
+ {
+ _client = client ?? throw new ArgumentNullException(nameof(client));
+ }
+ ///
+ /// Discount percentage applied to purchases.
+ ///
+ ///
+ /// Flag key: discountPercentage
+ /// Default value: 0.15
+ /// Type: double
+ ///
+ /// Optional context for the flag evaluation
+ /// Options for flag evaluation
+ /// The flag value
+ public async Task DiscountPercentageAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
+ {
+ return await _client.GetDoubleValueAsync("discountPercentage", 0.15, evaluationContext, options);
+ }
+
+ ///
+ /// Discount percentage applied to purchases.
+ ///
+ ///
+ /// Flag key: discountPercentage
+ /// Default value: 0.15
+ /// Type: double
+ ///
+ /// Optional context for the flag evaluation
+ /// Options for flag evaluation
+ /// The evaluation details containing the flag value and metadata
+ public async Task> DiscountPercentageDetailsAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
+ {
+ return await _client.GetDoubleDetailsAsync("discountPercentage", 0.15, evaluationContext, options);
+ }
+
+ ///
+ /// Controls whether Feature A is enabled.
+ ///
+ ///
+ /// Flag key: enableFeatureA
+ /// Default value: false
+ /// Type: bool
+ ///
+ /// Optional context for the flag evaluation
+ /// Options for flag evaluation
+ /// The flag value
+ public async Task EnableFeatureAAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
+ {
+ return await _client.GetBooleanValueAsync("enableFeatureA", false, evaluationContext, options);
+ }
+
+ ///
+ /// Controls whether Feature A is enabled.
+ ///
+ ///
+ /// Flag key: enableFeatureA
+ /// Default value: false
+ /// Type: bool
+ ///
+ /// Optional context for the flag evaluation
+ /// Options for flag evaluation
+ /// The evaluation details containing the flag value and metadata
+ public async Task> EnableFeatureADetailsAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
+ {
+ return await _client.GetBooleanDetailsAsync("enableFeatureA", false, evaluationContext, options);
+ }
+
+ ///
+ /// The message to use for greeting users.
+ ///
+ ///
+ /// Flag key: greetingMessage
+ /// Default value: Hello there!
+ /// Type: string
+ ///
+ /// Optional context for the flag evaluation
+ /// Options for flag evaluation
+ /// The flag value
+ public async Task GreetingMessageAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
+ {
+ return await _client.GetStringValueAsync("greetingMessage", "Hello there!", evaluationContext, options);
+ }
+
+ ///
+ /// The message to use for greeting users.
+ ///
+ ///
+ /// Flag key: greetingMessage
+ /// Default value: Hello there!
+ /// Type: string
+ ///
+ /// Optional context for the flag evaluation
+ /// Options for flag evaluation
+ /// The evaluation details containing the flag value and metadata
+ public async Task> GreetingMessageDetailsAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
+ {
+ return await _client.GetStringDetailsAsync("greetingMessage", "Hello there!", evaluationContext, options);
+ }
+
+ ///
+ /// Maximum allowed length for usernames.
+ ///
+ ///
+ /// Flag key: usernameMaxLength
+ /// Default value: 50
+ /// Type: int
+ ///
+ /// Optional context for the flag evaluation
+ /// Options for flag evaluation
+ /// The flag value
+ public async Task UsernameMaxLengthAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
+ {
+ return await _client.GetIntegerValueAsync("usernameMaxLength", 50, evaluationContext, options);
+ }
+
+ ///
+ /// Maximum allowed length for usernames.
+ ///
+ ///
+ /// Flag key: usernameMaxLength
+ /// Default value: 50
+ /// Type: int
+ ///
+ /// Optional context for the flag evaluation
+ /// Options for flag evaluation
+ /// The evaluation details containing the flag value and metadata
+ public async Task> UsernameMaxLengthDetailsAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
+ {
+ return await _client.GetIntegerDetailsAsync("usernameMaxLength", 50, evaluationContext, options);
+ }
+
+
+ ///
+ /// Creates a new GeneratedClient using the default OpenFeature client
+ ///
+ /// A new GeneratedClient instance
+ public static GeneratedClient CreateClient()
+ {
+ return new GeneratedClient(Api.Instance.GetClient());
+ }
+
+ ///
+ /// Creates a new GeneratedClient using a domain-specific OpenFeature client
+ ///
+ /// The domain to get the client for
+ /// A new GeneratedClient instance
+ public static GeneratedClient CreateClient(string domain)
+ {
+ return new GeneratedClient(Api.Instance.GetClient(domain));
+ }
+
+ ///
+ /// Creates a new GeneratedClient using a domain-specific OpenFeature client with context
+ ///
+ /// The domain to get the client for
+ /// Default context to use for evaluations
+ /// A new GeneratedClient instance
+ public static GeneratedClient CreateClient(string domain, EvaluationContext? evaluationContext = null)
+ {
+ return new GeneratedClient(Api.Instance.GetClient(domain));
+ }
+ }
+}
\ No newline at end of file
diff --git a/cmd/utils.go b/cmd/utils.go
index c5ef3b8..4e78e27 100644
--- a/cmd/utils.go
+++ b/cmd/utils.go
@@ -17,4 +17,4 @@ func printBanner() {
pterm.Println()
pterm.Printf("version: %s | compiled: %s\n", pterm.LightGreen(Version), pterm.LightGreen(Date))
pterm.Println(pterm.Cyan("🔗 https://openfeature.dev | https://github.com/open-feature/cli"))
-}
\ No newline at end of file
+}
diff --git a/cmd/version.go b/cmd/version.go
index dab3c41..a63c65d 100644
--- a/cmd/version.go
+++ b/cmd/version.go
@@ -38,4 +38,4 @@ func GetVersionCmd() *cobra.Command {
}
return versionCmd
-}
\ No newline at end of file
+}
diff --git a/docs/commands/openfeature_generate.md b/docs/commands/openfeature_generate.md
index 4a80d20..a2107dd 100644
--- a/docs/commands/openfeature_generate.md
+++ b/docs/commands/openfeature_generate.md
@@ -26,6 +26,7 @@ openfeature generate [flags]
### SEE ALSO
* [openfeature](openfeature.md) - CLI for OpenFeature.
+* [openfeature generate csharp](openfeature_generate_csharp.md) - Generate typesafe C# client.
* [openfeature generate go](openfeature_generate_go.md) - Generate typesafe accessors for OpenFeature.
* [openfeature generate nodejs](openfeature_generate_nodejs.md) - Generate typesafe Node.js client.
* [openfeature generate python](openfeature_generate_python.md) - Generate typesafe Python client.
diff --git a/docs/commands/openfeature_generate_csharp.md b/docs/commands/openfeature_generate_csharp.md
new file mode 100644
index 0000000..c72d3a1
--- /dev/null
+++ b/docs/commands/openfeature_generate_csharp.md
@@ -0,0 +1,37 @@
+
+
+## openfeature generate csharp
+
+Generate typesafe C# client.
+
+
+> **Stability**: alpha
+
+### Synopsis
+
+Generate typesafe C# client compatible with the OpenFeature .NET SDK.
+
+```
+openfeature generate csharp [flags]
+```
+
+### Options
+
+```
+ -h, --help help for csharp
+ --namespace string Namespace for the generated C# code (default "OpenFeature")
+```
+
+### Options inherited from parent commands
+
+```
+ --debug Enable debug logging
+ -m, --manifest string Path to the flag manifest (default "flags.json")
+ --no-input Disable interactive prompts
+ -o, --output string Path to where the generated files should be saved
+```
+
+### SEE ALSO
+
+* [openfeature generate](openfeature_generate.md) - Generate typesafe OpenFeature accessors.
+
diff --git a/internal/config/flags.go b/internal/config/flags.go
index 86e5dfb..2bb5526 100644
--- a/internal/config/flags.go
+++ b/internal/config/flags.go
@@ -6,19 +6,21 @@ import (
// Flag name constants to avoid duplication
const (
- DebugFlagName = "debug"
- ManifestFlagName = "manifest"
- OutputFlagName = "output"
- NoInputFlagName = "no-input"
- GoPackageFlagName = "package-name"
- OverrideFlagName = "override"
+ DebugFlagName = "debug"
+ ManifestFlagName = "manifest"
+ OutputFlagName = "output"
+ NoInputFlagName = "no-input"
+ GoPackageFlagName = "package-name"
+ CSharpNamespaceName = "namespace"
+ OverrideFlagName = "override"
)
// Default values for flags
const (
- DefaultManifestPath = "flags.json"
- DefaultOutputPath = ""
- DefaultGoPackageName = "openfeature"
+ DefaultManifestPath = "flags.json"
+ DefaultOutputPath = ""
+ DefaultGoPackageName = "openfeature"
+ DefaultCSharpNamespace = "OpenFeature"
)
// AddRootFlags adds the common flags to the given command
@@ -38,6 +40,11 @@ func AddGoGenerateFlags(cmd *cobra.Command) {
cmd.Flags().String(GoPackageFlagName, DefaultGoPackageName, "Name of the generated Go package")
}
+// AddCSharpGenerateFlags adds the C# generator specific flags to the given command
+func AddCSharpGenerateFlags(cmd *cobra.Command) {
+ cmd.Flags().String(CSharpNamespaceName, DefaultCSharpNamespace, "Namespace for the generated C# code")
+}
+
// AddInitFlags adds the init command specific flags
func AddInitFlags(cmd *cobra.Command) {
cmd.Flags().Bool(OverrideFlagName, false, "Override an existing configuration")
@@ -61,6 +68,12 @@ func GetGoPackageName(cmd *cobra.Command) string {
return goPackageName
}
+// GetCSharpNamespace gets the C# namespace from the given command
+func GetCSharpNamespace(cmd *cobra.Command) string {
+ namespace, _ := cmd.Flags().GetString(CSharpNamespaceName)
+ return namespace
+}
+
// GetNoInput gets the no-input flag from the given command
func GetNoInput(cmd *cobra.Command) bool {
noInput, _ := cmd.Flags().GetBool(NoInputFlagName)
diff --git a/internal/flagset/flagset.go b/internal/flagset/flagset.go
index 8b4aff0..33d3155 100644
--- a/internal/flagset/flagset.go
+++ b/internal/flagset/flagset.go
@@ -27,24 +27,24 @@ const (
func (f FlagType) String() string {
switch f {
case IntType:
- return "int"
+ return "int"
case FloatType:
- return "float"
+ return "float"
case BoolType:
- return "bool"
+ return "bool"
case StringType:
- return "string"
+ return "string"
case ObjectType:
- return "object"
+ return "object"
default:
- return "unknown"
+ return "unknown"
}
}
type Flag struct {
- Key string
- Type FlagType
- Description string
+ Key string
+ Type FlagType
+ Description string
DefaultValue any
}
@@ -90,9 +90,9 @@ func (fs *Flagset) Filter(unsupportedFlagTypes map[FlagType]bool) *Flagset {
func (fs *Flagset) UnmarshalJSON(data []byte) error {
var manifest struct {
Flags map[string]struct {
- FlagType string `json:"flagType"`
- Description string `json:"description"`
- DefaultValue any `json:"defaultValue"`
+ FlagType string `json:"flagType"`
+ Description string `json:"description"`
+ DefaultValue any `json:"defaultValue"`
} `json:"flags"`
}
@@ -131,4 +131,4 @@ func (fs *Flagset) UnmarshalJSON(data []byte) error {
})
return nil
-}
\ No newline at end of file
+}
diff --git a/internal/generators/csharp/csharp.go b/internal/generators/csharp/csharp.go
new file mode 100644
index 0000000..3c79d55
--- /dev/null
+++ b/internal/generators/csharp/csharp.go
@@ -0,0 +1,74 @@
+package csharp
+
+import (
+ _ "embed"
+ "fmt"
+ "text/template"
+
+ "github.com/open-feature/cli/internal/flagset"
+ "github.com/open-feature/cli/internal/generators"
+)
+
+type CsharpGenerator struct {
+ generators.CommonGenerator
+}
+
+type Params struct {
+ // Add C# specific parameters here if needed
+ Namespace string
+}
+
+//go:embed csharp.tmpl
+var csharpTmpl string
+
+func openFeatureType(t flagset.FlagType) string {
+ switch t {
+ case flagset.IntType:
+ return "int"
+ case flagset.FloatType:
+ return "double" // .NET uses double, not float
+ case flagset.BoolType:
+ return "bool"
+ case flagset.StringType:
+ return "string"
+ default:
+ return ""
+ }
+}
+
+func formatDefaultValue(flag flagset.Flag) string {
+ switch flag.Type {
+ case flagset.StringType:
+ return fmt.Sprintf("\"%s\"", flag.DefaultValue)
+ case flagset.BoolType:
+ if flag.DefaultValue == true {
+ return "true"
+ }
+ return "false"
+ default:
+ return fmt.Sprintf("%v", flag.DefaultValue)
+ }
+}
+
+func (g *CsharpGenerator) Generate(params *generators.Params[Params]) error {
+ funcs := template.FuncMap{
+ "OpenFeatureType": openFeatureType,
+ "FormatDefaultValue": formatDefaultValue,
+ }
+
+ newParams := &generators.Params[any]{
+ OutputPath: params.OutputPath,
+ Custom: params.Custom,
+ }
+
+ return g.GenerateFile(funcs, csharpTmpl, newParams, "OpenFeature.g.cs")
+}
+
+// NewGenerator creates a generator for C#.
+func NewGenerator(fs *flagset.Flagset) *CsharpGenerator {
+ return &CsharpGenerator{
+ CommonGenerator: *generators.NewGenerator(fs, map[flagset.FlagType]bool{
+ flagset.ObjectType: true,
+ }),
+ }
+}
diff --git a/internal/generators/csharp/csharp.tmpl b/internal/generators/csharp/csharp.tmpl
new file mode 100644
index 0000000..39d620d
--- /dev/null
+++ b/internal/generators/csharp/csharp.tmpl
@@ -0,0 +1,145 @@
+// AUTOMATICALLY GENERATED BY OPENFEATURE CLI, DO NOT EDIT.
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using System.Threading;
+using Microsoft.Extensions.DependencyInjection;
+using OpenFeature;
+using OpenFeature.Model;
+
+namespace {{ if .Params.Custom.Namespace }}{{ .Params.Custom.Namespace }}{{ else }}OpenFeatureGenerated{{ end }}
+{
+ ///
+ /// Service collection extensions for OpenFeature
+ ///
+ public static class OpenFeatureServiceExtensions
+ {
+ ///
+ /// Adds OpenFeature services to the service collection with the generated client
+ ///
+ /// The service collection to add services to
+ /// The service collection for chaining
+ public static IServiceCollection AddOpenFeature(this IServiceCollection services)
+ {
+ return services
+ .AddSingleton(_ => Api.Instance)
+ .AddSingleton(provider => provider.GetRequiredService().GetClient())
+ .AddSingleton();
+ }
+
+ ///
+ /// Adds OpenFeature services to the service collection with the generated client for a specific domain
+ ///
+ /// The service collection to add services to
+ /// The domain to get the client for
+ /// The service collection for chaining
+ public static IServiceCollection AddOpenFeature(this IServiceCollection services, string domain)
+ {
+ return services
+ .AddSingleton(_ => Api.Instance)
+ .AddSingleton(provider => provider.GetRequiredService().GetClient(domain))
+ .AddSingleton();
+ }
+ }
+
+ ///
+ /// Generated OpenFeature client for typesafe flag access
+ ///
+ public class GeneratedClient
+ {
+ private readonly IFeatureClient _client;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The OpenFeature client to use for flag evaluations.
+ public GeneratedClient(IFeatureClient client)
+ {
+ _client = client ?? throw new ArgumentNullException(nameof(client));
+ }
+
+ {{- range .Flagset.Flags }}
+ ///
+ /// {{ .Description }}
+ ///
+ ///
+ /// Flag key: {{ .Key }}
+ /// Default value: {{ .DefaultValue }}
+ /// Type: {{ .Type | OpenFeatureType }}
+ ///
+ /// Optional context for the flag evaluation
+ /// Options for flag evaluation
+ /// The flag value
+ public async Task<{{ .Type | OpenFeatureType }}> {{ .Key | ToPascal }}Async(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
+ {
+ {{- if eq .Type 1 }}
+ return await _client.GetIntegerValueAsync("{{ .Key }}", {{ . | FormatDefaultValue }}, evaluationContext, options);
+ {{- else if eq .Type 2 }}
+ return await _client.GetDoubleValueAsync("{{ .Key }}", {{ . | FormatDefaultValue }}, evaluationContext, options);
+ {{- else if eq .Type 3 }}
+ return await _client.GetBooleanValueAsync("{{ .Key }}", {{ . | FormatDefaultValue }}, evaluationContext, options);
+ {{- else if eq .Type 4 }}
+ return await _client.GetStringValueAsync("{{ .Key }}", {{ . | FormatDefaultValue }}, evaluationContext, options);
+ {{- else }}
+ throw new NotSupportedException("Unsupported flag type");
+ {{- end }}
+ }
+
+ ///
+ /// {{ .Description }}
+ ///
+ ///
+ /// Flag key: {{ .Key }}
+ /// Default value: {{ .DefaultValue }}
+ /// Type: {{ .Type | OpenFeatureType }}
+ ///
+ /// Optional context for the flag evaluation
+ /// Options for flag evaluation
+ /// The evaluation details containing the flag value and metadata
+ public async Task> {{ .Key | ToPascal }}DetailsAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
+ {
+ {{- if eq .Type 1 }}
+ return await _client.GetIntegerDetailsAsync("{{ .Key }}", {{ . | FormatDefaultValue }}, evaluationContext, options);
+ {{- else if eq .Type 2 }}
+ return await _client.GetDoubleDetailsAsync("{{ .Key }}", {{ . | FormatDefaultValue }}, evaluationContext, options);
+ {{- else if eq .Type 3 }}
+ return await _client.GetBooleanDetailsAsync("{{ .Key }}", {{ . | FormatDefaultValue }}, evaluationContext, options);
+ {{- else if eq .Type 4 }}
+ return await _client.GetStringDetailsAsync("{{ .Key }}", {{ . | FormatDefaultValue }}, evaluationContext, options);
+ {{- else }}
+ throw new NotSupportedException("Unsupported flag type");
+ {{- end }}
+ }
+ {{ end }}
+
+ ///
+ /// Creates a new GeneratedClient using the default OpenFeature client
+ ///
+ /// A new GeneratedClient instance
+ public static GeneratedClient CreateClient()
+ {
+ return new GeneratedClient(Api.Instance.GetClient());
+ }
+
+ ///
+ /// Creates a new GeneratedClient using a domain-specific OpenFeature client
+ ///
+ /// The domain to get the client for
+ /// A new GeneratedClient instance
+ public static GeneratedClient CreateClient(string domain)
+ {
+ return new GeneratedClient(Api.Instance.GetClient(domain));
+ }
+
+ ///
+ /// Creates a new GeneratedClient using a domain-specific OpenFeature client with context
+ ///
+ /// The domain to get the client for
+ /// Default context to use for evaluations
+ /// A new GeneratedClient instance
+ public static GeneratedClient CreateClient(string domain, EvaluationContext? evaluationContext = null)
+ {
+ return new GeneratedClient(Api.Instance.GetClient(domain));
+ }
+ }
+}
\ No newline at end of file
diff --git a/internal/generators/func.go b/internal/generators/func.go
index ac7b877..abe3de8 100644
--- a/internal/generators/func.go
+++ b/internal/generators/func.go
@@ -17,16 +17,16 @@ func defaultFuncs() template.FuncMap {
"ToPascal": strcase.ToCamel,
// Remapping ToLowerCamel to ToCamel to match the expected behavior
// Ref: See above
- "ToCamel": strcase.ToLowerCamel,
- "ToKebab": strcase.ToKebab,
+ "ToCamel": strcase.ToLowerCamel,
+ "ToKebab": strcase.ToKebab,
"ToScreamingKebab": strcase.ToScreamingKebab,
- "ToSnake": strcase.ToSnake,
+ "ToSnake": strcase.ToSnake,
"ToScreamingSnake": strcase.ToScreamingSnake,
- "ToUpper": strings.ToUpper,
- "ToLower": strings.ToLower,
- "Title": cases.Title,
- "Quote": strconv.Quote,
- "QuoteString": func (input any) any {
+ "ToUpper": strings.ToUpper,
+ "ToLower": strings.ToLower,
+ "Title": cases.Title,
+ "Quote": strconv.Quote,
+ "QuoteString": func(input any) any {
if str, ok := input.(string); ok {
return strconv.Quote(str)
}
@@ -39,4 +39,4 @@ func init() {
// results in "Api" using ToCamel("API")
// results in "api" using ToLowerCamel("API")
strcase.ConfigureAcronym("API", "api")
-}
\ No newline at end of file
+}
diff --git a/internal/generators/generators.go b/internal/generators/generators.go
index 7a11fba..31c6921 100644
--- a/internal/generators/generators.go
+++ b/internal/generators/generators.go
@@ -48,7 +48,7 @@ func (g *CommonGenerator) GenerateFile(customFunc template.FuncMap, tmpl string,
maps.Copy(funcs, customFunc)
logger.Default.Debug(fmt.Sprintf("Generating file: %s", name))
-
+
generatorTemplate, err := template.New("generator").Funcs(funcs).Parse(tmpl)
if err != nil {
return fmt.Errorf("error initializing template: %v", err)
@@ -68,9 +68,9 @@ func (g *CommonGenerator) GenerateFile(customFunc template.FuncMap, tmpl string,
logger.Default.FileFailed(fullPath, err)
return err
}
-
+
// Log successful file creation
logger.Default.FileCreated(fullPath)
-
+
return nil
}
diff --git a/internal/generators/manager.go b/internal/generators/manager.go
index d6c1c8f..737565b 100644
--- a/internal/generators/manager.go
+++ b/internal/generators/manager.go
@@ -12,10 +12,10 @@ type GeneratorCreator func() *cobra.Command
// GeneratorInfo contains metadata about a generator
type GeneratorInfo struct {
- Name string
- Description string
- Stability Stability
- Creator GeneratorCreator
+ Name string
+ Description string
+ Stability Stability
+ Creator GeneratorCreator
}
// GeneratorManager maintains a registry of available generators
@@ -34,10 +34,10 @@ func NewGeneratorManager() *GeneratorManager {
func (m *GeneratorManager) Register(cmdCreator func() *cobra.Command) {
cmd := cmdCreator()
m.generators[cmd.Use] = GeneratorInfo{
- Name: cmd.Use,
- Description: cmd.Short,
- Stability: Stability(cmd.Annotations["stability"]),
- Creator: cmdCreator,
+ Name: cmd.Use,
+ Description: cmd.Short,
+ Stability: Stability(cmd.Annotations["stability"]),
+ Creator: cmdCreator,
}
}
@@ -49,11 +49,11 @@ func (m *GeneratorManager) GetAll() map[string]GeneratorInfo {
// GetCommands returns cobra commands for all registered generators
func (m *GeneratorManager) GetCommands() []*cobra.Command {
var commands []*cobra.Command
-
+
for _, info := range m.generators {
commands = append(commands, info.Creator())
}
-
+
return commands
}
@@ -62,14 +62,14 @@ func (m *GeneratorManager) PrintGeneratorsTable() error {
tableData := [][]string{
{"Generator", "Description", "Stability"},
}
-
+
// Get generator names for consistent ordering
var names []string
for name := range m.generators {
names = append(names, name)
}
sort.Strings(names)
-
+
for _, name := range names {
info := m.generators[name]
tableData = append(tableData, []string{
@@ -78,7 +78,7 @@ func (m *GeneratorManager) PrintGeneratorsTable() error {
string(info.Stability),
})
}
-
+
return pterm.DefaultTable.WithHasHeader().WithData(tableData).Render()
}
diff --git a/internal/manifest/json-schema.go b/internal/manifest/json-schema.go
index 4ba383b..45dc746 100644
--- a/internal/manifest/json-schema.go
+++ b/internal/manifest/json-schema.go
@@ -63,9 +63,9 @@ type Manifest struct {
// Converts the Manifest struct to a JSON schema.
func ToJSONSchema() *jsonschema.Schema {
reflector := &jsonschema.Reflector{
- ExpandedStruct: true,
+ ExpandedStruct: true,
AllowAdditionalProperties: true,
- BaseSchemaID: "openfeature-cli",
+ BaseSchemaID: "openfeature-cli",
}
if err := reflector.AddGoComments("github.com/open-feature/cli", "./internal/manifest"); err != nil {
@@ -121,4 +121,4 @@ func ToJSONSchema() *jsonschema.Schema {
}
return schema
-}
\ No newline at end of file
+}
diff --git a/schema/generate-schema.go b/schema/generate-schema.go
index 7c5f31c..2f766d3 100644
--- a/schema/generate-schema.go
+++ b/schema/generate-schema.go
@@ -29,8 +29,8 @@ func main() {
defer file.Close()
if _, err := file.Write(data); err != nil {
- log.Fatal(fmt.Errorf("failed to write JSON schema to file: %w", err));
+ log.Fatal(fmt.Errorf("failed to write JSON schema to file: %w", err))
}
fmt.Println("JSON schema generated successfully at " + schemaPath)
-}
\ No newline at end of file
+}
diff --git a/schema/v0/schema_test.go b/schema/v0/schema_test.go
index 05d039f..9e9c08b 100644
--- a/schema/v0/schema_test.go
+++ b/schema/v0/schema_test.go
@@ -49,11 +49,11 @@ func walkPath(shouldPass bool, root string) error {
schemaLoader := gojsonschema.NewStringLoader(SchemaFile)
manifestLoader := gojsonschema.NewGoLoader(v)
result, err := gojsonschema.Validate(schemaLoader, manifestLoader)
- if (err != nil) {
+ if err != nil {
return fmt.Errorf("Error validating json schema: %v", err)
}
- if (len(result.Errors()) >= 1 && shouldPass == true) {
+ if len(result.Errors()) >= 1 && shouldPass == true {
var errorMessage strings.Builder
errorMessage.WriteString("file " + path + " should be valid, but had the following issues:\n")
@@ -63,7 +63,7 @@ func walkPath(shouldPass bool, root string) error {
return fmt.Errorf("%s", errorMessage.String())
}
- if (len(result.Errors()) == 0 && shouldPass == false) {
+ if len(result.Errors()) == 0 && shouldPass == false {
return fmt.Errorf("file %s should be invalid, but no issues were detected", path)
}
diff --git a/test/csharp-integration/CompileTest.csproj b/test/csharp-integration/CompileTest.csproj
new file mode 100644
index 0000000..98c3cbb
--- /dev/null
+++ b/test/csharp-integration/CompileTest.csproj
@@ -0,0 +1,15 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/csharp-integration/Dockerfile b/test/csharp-integration/Dockerfile
new file mode 100644
index 0000000..3ef09a8
--- /dev/null
+++ b/test/csharp-integration/Dockerfile
@@ -0,0 +1,17 @@
+FROM mcr.microsoft.com/dotnet/sdk:8.0
+
+WORKDIR /app
+
+# Copy necessary files
+COPY expected/OpenFeature.cs /app/
+COPY CompileTest.csproj /app/
+COPY Program.cs /app/
+
+# Restore dependencies
+RUN dotnet restore
+
+# Build the project
+RUN dotnet build
+
+# The image will be used to validate C# compilation only
+ENTRYPOINT ["dotnet", "run"]
\ No newline at end of file
diff --git a/test/csharp-integration/OpenFeature.cs b/test/csharp-integration/OpenFeature.cs
new file mode 100644
index 0000000..80603d7
--- /dev/null
+++ b/test/csharp-integration/OpenFeature.cs
@@ -0,0 +1,176 @@
+// AUTOMATICALLY GENERATED BY OPENFEATURE CLI, DO NOT EDIT.
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using OpenFeature;
+using OpenFeature.Model;
+
+namespace OpenFeature
+{
+ ///
+ /// Generated OpenFeature client for typesafe flag access
+ ///
+ public class GeneratedClient
+ {
+ private readonly IFeatureClient _client;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The OpenFeature client to use for flag evaluations.
+ public GeneratedClient(IFeatureClient client)
+ {
+ _client = client ?? throw new ArgumentNullException(nameof(client));
+ }
+ ///
+ /// Discount percentage applied to purchases.
+ ///
+ ///
+ /// Flag key: discountPercentage
+ /// Default value: 0.15
+ /// Type: float
+ ///
+ /// Optional context for the flag evaluation
+ /// The flag value
+ public async Task DiscountPercentageAsync(EvaluationContext evaluationContext = null)
+ {
+ return await _client.GetFloatValueAsync("discountPercentage", 0.15, evaluationContext);
+ }
+
+ ///
+ /// Discount percentage applied to purchases.
+ ///
+ ///
+ /// Flag key: discountPercentage
+ /// Default value: 0.15
+ /// Type: float
+ ///
+ /// Optional context for the flag evaluation
+ /// The evaluation details containing the flag value and metadata
+ public async Task> DiscountPercentageDetailsAsync(EvaluationContext evaluationContext = null)
+ {
+ return await _client.GetFloatDetailsAsync("discountPercentage", 0.15, evaluationContext);
+ }
+
+ ///
+ /// Controls whether Feature A is enabled.
+ ///
+ ///
+ /// Flag key: enableFeatureA
+ /// Default value: false
+ /// Type: bool
+ ///
+ /// Optional context for the flag evaluation
+ /// The flag value
+ public async Task EnableFeatureAAsync(EvaluationContext evaluationContext = null)
+ {
+ return await _client.GetBoolValueAsync("enableFeatureA", false, evaluationContext);
+ }
+
+ ///
+ /// Controls whether Feature A is enabled.
+ ///
+ ///
+ /// Flag key: enableFeatureA
+ /// Default value: false
+ /// Type: bool
+ ///
+ /// Optional context for the flag evaluation
+ /// The evaluation details containing the flag value and metadata
+ public async Task> EnableFeatureADetailsAsync(EvaluationContext evaluationContext = null)
+ {
+ return await _client.GetBoolDetailsAsync("enableFeatureA", false, evaluationContext);
+ }
+
+ ///
+ /// The message to use for greeting users.
+ ///
+ ///
+ /// Flag key: greetingMessage
+ /// Default value: Hello there!
+ /// Type: string
+ ///
+ /// Optional context for the flag evaluation
+ /// The flag value
+ public async Task GreetingMessageAsync(EvaluationContext evaluationContext = null)
+ {
+ return await _client.GetStringValueAsync("greetingMessage", "Hello there!", evaluationContext);
+ }
+
+ ///
+ /// The message to use for greeting users.
+ ///
+ ///
+ /// Flag key: greetingMessage
+ /// Default value: Hello there!
+ /// Type: string
+ ///
+ /// Optional context for the flag evaluation
+ /// The evaluation details containing the flag value and metadata
+ public async Task> GreetingMessageDetailsAsync(EvaluationContext evaluationContext = null)
+ {
+ return await _client.GetStringDetailsAsync("greetingMessage", "Hello there!", evaluationContext);
+ }
+
+ ///
+ /// Maximum allowed length for usernames.
+ ///
+ ///
+ /// Flag key: usernameMaxLength
+ /// Default value: 50
+ /// Type: int
+ ///
+ /// Optional context for the flag evaluation
+ /// The flag value
+ public async Task UsernameMaxLengthAsync(EvaluationContext evaluationContext = null)
+ {
+ return await _client.GetIntValueAsync("usernameMaxLength", 50, evaluationContext);
+ }
+
+ ///
+ /// Maximum allowed length for usernames.
+ ///
+ ///
+ /// Flag key: usernameMaxLength
+ /// Default value: 50
+ /// Type: int
+ ///
+ /// Optional context for the flag evaluation
+ /// The evaluation details containing the flag value and metadata
+ public async Task> UsernameMaxLengthDetailsAsync(EvaluationContext evaluationContext = null)
+ {
+ return await _client.GetIntDetailsAsync("usernameMaxLength", 50, evaluationContext);
+ }
+
+
+ ///
+ /// Creates a new GeneratedClient using the default OpenFeature client
+ ///
+ /// A new GeneratedClient instance
+ public static GeneratedClient CreateClient()
+ {
+ return new GeneratedClient(Api.GetClient());
+ }
+
+ ///
+ /// Creates a new GeneratedClient using a domain-specific OpenFeature client
+ ///
+ /// The domain to get the client for
+ /// A new GeneratedClient instance
+ public static GeneratedClient CreateClient(string domain)
+ {
+ return new GeneratedClient(Api.GetClient(domain));
+ }
+
+ ///
+ /// Creates a new GeneratedClient using a domain-specific OpenFeature client with context
+ ///
+ /// The domain to get the client for
+ /// Default context to use for evaluations
+ /// A new GeneratedClient instance
+ public static GeneratedClient CreateClient(string domain, EvaluationContext evaluationContext)
+ {
+ return new GeneratedClient(Api.GetClient(domain, evaluationContext));
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/csharp-integration/Program.cs b/test/csharp-integration/Program.cs
new file mode 100644
index 0000000..03bd66d
--- /dev/null
+++ b/test/csharp-integration/Program.cs
@@ -0,0 +1,36 @@
+using System;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using OpenFeature;
+using OpenFeature.Model;
+using TestNamespace;
+
+// This program just validates that the generated OpenFeature C# client code compiles
+// We don't need to run the code since the goal is to test compilation only
+namespace CompileTest
+{
+ class Program
+ {
+ static void Main(string[] args)
+ {
+ Console.WriteLine("Testing compilation of generated OpenFeature client...");
+
+ // Test DI initialization
+ var services = new ServiceCollection();
+ // Register OpenFeature services manually for the test
+ services.AddSingleton(_ => Api.Instance);
+ services.AddSingleton(_ => Api.Instance.GetClient());
+ services.AddSingleton();
+ var serviceProvider = services.BuildServiceProvider();
+
+ // Test client retrieval from DI
+ var client = serviceProvider.GetRequiredService();
+
+ // Also test the traditional factory method
+ var clientFromFactory = GeneratedClient.CreateClient();
+
+ // Success!
+ Console.WriteLine("Generated C# code compiles successfully!");
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/csharp-integration/expected/OpenFeature.cs b/test/csharp-integration/expected/OpenFeature.cs
new file mode 100644
index 0000000..b20990a
--- /dev/null
+++ b/test/csharp-integration/expected/OpenFeature.cs
@@ -0,0 +1,252 @@
+// AUTOMATICALLY GENERATED BY OPENFEATURE CLI, DO NOT EDIT.
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using System.Threading;
+using Microsoft.Extensions.DependencyInjection;
+using OpenFeature;
+using OpenFeature.Model;
+
+
+namespace TestNamespace
+{
+ ///
+ /// Service collection extensions for OpenFeature
+ ///
+ public static class OpenFeatureServiceExtensions
+ {
+ ///
+ /// Adds OpenFeature services to the service collection with the generated client
+ ///
+ /// The service collection to add services to
+ /// The service collection for chaining
+ public static IServiceCollection AddOpenFeature(this IServiceCollection services)
+ {
+ return services
+ .AddSingleton(_ => Api.Instance)
+ .AddSingleton(provider => provider.GetRequiredService().GetClient())
+ .AddSingleton();
+ }
+
+ ///
+ /// Adds OpenFeature services to the service collection with the generated client for a specific domain
+ ///
+ /// The service collection to add services to
+ /// The domain to get the client for
+ /// The service collection for chaining
+ public static IServiceCollection AddOpenFeature(this IServiceCollection services, string domain)
+ {
+ return services
+ .AddSingleton(_ => Api.Instance)
+ .AddSingleton(provider => provider.GetRequiredService().GetClient(domain))
+ .AddSingleton();
+ }
+ }
+
+ ///
+ /// Generated OpenFeature client for typesafe flag access
+ ///
+ public class GeneratedClient
+ {
+ private readonly IFeatureClient _client;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The OpenFeature client to use for flag evaluations.
+ public GeneratedClient(IFeatureClient client)
+ {
+ _client = client ?? throw new ArgumentNullException(nameof(client));
+ }
+ ///
+ /// Discount percentage applied to purchases.
+ ///
+ ///
+ /// Flag key: discountPercentage
+ /// Default value: 0.15
+ /// Type: double
+ ///
+ /// Optional context for the flag evaluation
+ /// Options for flag evaluation
+ /// The flag value
+ public async Task DiscountPercentageAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
+ {
+ return await _client.GetDoubleValueAsync("discountPercentage", 0.15, evaluationContext, options);
+ }
+
+ ///
+ /// Discount percentage applied to purchases.
+ ///
+ ///
+ /// Flag key: discountPercentage
+ /// Default value: 0.15
+ /// Type: double
+ ///
+ /// Optional context for the flag evaluation
+ /// Options for flag evaluation
+ /// The evaluation details containing the flag value and metadata
+ public async Task> DiscountPercentageDetailsAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
+ {
+ return await _client.GetDoubleDetailsAsync("discountPercentage", 0.15, evaluationContext, options);
+ }
+
+ ///
+ /// Controls whether Feature A is enabled.
+ ///
+ ///
+ /// Flag key: enableFeatureA
+ /// Default value: false
+ /// Type: bool
+ ///
+ /// Optional context for the flag evaluation
+ /// Options for flag evaluation
+ /// The flag value
+ public async Task EnableFeatureAAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
+ {
+ return await _client.GetBooleanValueAsync("enableFeatureA", false, evaluationContext, options);
+ }
+
+ ///
+ /// Controls whether Feature A is enabled.
+ ///
+ ///
+ /// Flag key: enableFeatureA
+ /// Default value: false
+ /// Type: bool
+ ///
+ /// Optional context for the flag evaluation
+ /// Options for flag evaluation
+ /// The evaluation details containing the flag value and metadata
+ public async Task> EnableFeatureADetailsAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
+ {
+ return await _client.GetBooleanDetailsAsync("enableFeatureA", false, evaluationContext, options);
+ }
+
+ ///
+ /// The message to use for greeting users.
+ ///
+ ///
+ /// Flag key: greetingMessage
+ /// Default value: Hello there!
+ /// Type: string
+ ///
+ /// Optional context for the flag evaluation
+ /// Options for flag evaluation
+ /// The flag value
+ public async Task GreetingMessageAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
+ {
+ return await _client.GetStringValueAsync("greetingMessage", "Hello there!", evaluationContext, options);
+ }
+
+ ///
+ /// The message to use for greeting users.
+ ///
+ ///
+ /// Flag key: greetingMessage
+ /// Default value: Hello there!
+ /// Type: string
+ ///
+ /// Optional context for the flag evaluation
+ /// Options for flag evaluation
+ /// The evaluation details containing the flag value and metadata
+ public async Task> GreetingMessageDetailsAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
+ {
+ return await _client.GetStringDetailsAsync("greetingMessage", "Hello there!", evaluationContext, options);
+ }
+
+ ///
+ /// Allows customization of theme colors.
+ ///
+ ///
+ /// Flag key: themeCustomization
+ /// Default value: map[primaryColor:#007bff secondaryColor:#6c757d]
+ /// Type: Value
+ ///
+ /// Optional context for the flag evaluation
+ /// Options for flag evaluation
+ /// The flag value
+ public async Task ThemeCustomizationAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
+ {
+ throw new NotSupportedException("Unsupported flag type");
+ }
+
+ ///
+ /// Allows customization of theme colors.
+ ///
+ ///
+ /// Flag key: themeCustomization
+ /// Default value: map[primaryColor:#007bff secondaryColor:#6c757d]
+ /// Type: Value
+ ///
+ /// Optional context for the flag evaluation
+ /// Options for flag evaluation
+ /// The evaluation details containing the flag value and metadata
+ public async Task> ThemeCustomizationDetailsAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
+ {
+ throw new NotSupportedException("Unsupported flag type");
+ }
+
+ ///
+ /// Maximum allowed length for usernames.
+ ///
+ ///
+ /// Flag key: usernameMaxLength
+ /// Default value: 50
+ /// Type: int
+ ///
+ /// Optional context for the flag evaluation
+ /// Options for flag evaluation
+ /// The flag value
+ public async Task UsernameMaxLengthAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
+ {
+ return await _client.GetIntegerValueAsync("usernameMaxLength", 50, evaluationContext, options);
+ }
+
+ ///
+ /// Maximum allowed length for usernames.
+ ///
+ ///
+ /// Flag key: usernameMaxLength
+ /// Default value: 50
+ /// Type: int
+ ///
+ /// Optional context for the flag evaluation
+ /// Options for flag evaluation
+ /// The evaluation details containing the flag value and metadata
+ public async Task> UsernameMaxLengthDetailsAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
+ {
+ return await _client.GetIntegerDetailsAsync("usernameMaxLength", 50, evaluationContext, options);
+ }
+
+
+ ///
+ /// Creates a new GeneratedClient using the default OpenFeature client
+ ///
+ /// A new GeneratedClient instance
+ public static GeneratedClient CreateClient()
+ {
+ return new GeneratedClient(Api.Instance.GetClient());
+ }
+
+ ///
+ /// Creates a new GeneratedClient using a domain-specific OpenFeature client
+ ///
+ /// The domain to get the client for
+ /// A new GeneratedClient instance
+ public static GeneratedClient CreateClient(string domain)
+ {
+ return new GeneratedClient(Api.Instance.GetClient(domain));
+ }
+
+ ///
+ /// Creates a new GeneratedClient using a domain-specific OpenFeature client with context
+ ///
+ /// The domain to get the client for
+ /// Default context to use for evaluations
+ /// A new GeneratedClient instance
+ public static GeneratedClient CreateClient(string domain, EvaluationContext? evaluationContext = null)
+ {
+ return new GeneratedClient(Api.Instance.GetClient(domain));
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/csharp-integration/expected/OpenFeature.g.cs b/test/csharp-integration/expected/OpenFeature.g.cs
new file mode 100644
index 0000000..28b6de1
--- /dev/null
+++ b/test/csharp-integration/expected/OpenFeature.g.cs
@@ -0,0 +1,219 @@
+// AUTOMATICALLY GENERATED BY OPENFEATURE CLI, DO NOT EDIT.
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using System.Threading;
+using Microsoft.Extensions.DependencyInjection;
+using OpenFeature;
+using OpenFeature.Model;
+
+namespace TestNamespace
+{
+ ///
+ /// Service collection extensions for OpenFeature
+ ///
+ public static class OpenFeatureServiceExtensions
+ {
+ ///
+ /// Adds OpenFeature services to the service collection with the generated client
+ ///
+ /// The service collection to add services to
+ /// The service collection for chaining
+ public static IServiceCollection AddOpenFeature(this IServiceCollection services)
+ {
+ return services
+ .AddSingleton(_ => Api.Instance)
+ .AddSingleton(provider => provider.GetRequiredService().GetClient())
+ .AddSingleton();
+ }
+
+ ///
+ /// Adds OpenFeature services to the service collection with the generated client for a specific domain
+ ///
+ /// The service collection to add services to
+ /// The domain to get the client for
+ /// The service collection for chaining
+ public static IServiceCollection AddOpenFeature(this IServiceCollection services, string domain)
+ {
+ return services
+ .AddSingleton(_ => Api.Instance)
+ .AddSingleton(provider => provider.GetRequiredService().GetClient(domain))
+ .AddSingleton();
+ }
+ }
+
+ ///
+ /// Generated OpenFeature client for typesafe flag access
+ ///
+ public class GeneratedClient
+ {
+ private readonly IFeatureClient _client;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The OpenFeature client to use for flag evaluations.
+ public GeneratedClient(IFeatureClient client)
+ {
+ _client = client ?? throw new ArgumentNullException(nameof(client));
+ }
+ ///
+ /// Discount percentage applied to purchases.
+ ///
+ ///
+ /// Flag key: discountPercentage
+ /// Default value: 0.15
+ /// Type: double
+ ///
+ /// Optional context for the flag evaluation
+ /// Options for flag evaluation
+ /// The flag value
+ public async Task DiscountPercentageAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
+ {
+ return await _client.GetDoubleValueAsync("discountPercentage", 0.15, evaluationContext, options);
+ }
+
+ ///
+ /// Discount percentage applied to purchases.
+ ///
+ ///
+ /// Flag key: discountPercentage
+ /// Default value: 0.15
+ /// Type: double
+ ///
+ /// Optional context for the flag evaluation
+ /// Options for flag evaluation
+ /// The evaluation details containing the flag value and metadata
+ public async Task> DiscountPercentageDetailsAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
+ {
+ return await _client.GetDoubleDetailsAsync("discountPercentage", 0.15, evaluationContext, options);
+ }
+
+ ///
+ /// Controls whether Feature A is enabled.
+ ///
+ ///
+ /// Flag key: enableFeatureA
+ /// Default value: false
+ /// Type: bool
+ ///
+ /// Optional context for the flag evaluation
+ /// Options for flag evaluation
+ /// The flag value
+ public async Task EnableFeatureAAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
+ {
+ return await _client.GetBooleanValueAsync("enableFeatureA", false, evaluationContext, options);
+ }
+
+ ///
+ /// Controls whether Feature A is enabled.
+ ///
+ ///
+ /// Flag key: enableFeatureA
+ /// Default value: false
+ /// Type: bool
+ ///
+ /// Optional context for the flag evaluation
+ /// Options for flag evaluation
+ /// The evaluation details containing the flag value and metadata
+ public async Task> EnableFeatureADetailsAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
+ {
+ return await _client.GetBooleanDetailsAsync("enableFeatureA", false, evaluationContext, options);
+ }
+
+ ///
+ /// The message to use for greeting users.
+ ///
+ ///
+ /// Flag key: greetingMessage
+ /// Default value: Hello there!
+ /// Type: string
+ ///
+ /// Optional context for the flag evaluation
+ /// Options for flag evaluation
+ /// The flag value
+ public async Task GreetingMessageAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
+ {
+ return await _client.GetStringValueAsync("greetingMessage", "Hello there!", evaluationContext, options);
+ }
+
+ ///
+ /// The message to use for greeting users.
+ ///
+ ///
+ /// Flag key: greetingMessage
+ /// Default value: Hello there!
+ /// Type: string
+ ///
+ /// Optional context for the flag evaluation
+ /// Options for flag evaluation
+ /// The evaluation details containing the flag value and metadata
+ public async Task> GreetingMessageDetailsAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
+ {
+ return await _client.GetStringDetailsAsync("greetingMessage", "Hello there!", evaluationContext, options);
+ }
+
+ ///
+ /// Maximum allowed length for usernames.
+ ///
+ ///
+ /// Flag key: usernameMaxLength
+ /// Default value: 50
+ /// Type: int
+ ///
+ /// Optional context for the flag evaluation
+ /// Options for flag evaluation
+ /// The flag value
+ public async Task UsernameMaxLengthAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
+ {
+ return await _client.GetIntegerValueAsync("usernameMaxLength", 50, evaluationContext, options);
+ }
+
+ ///
+ /// Maximum allowed length for usernames.
+ ///
+ ///
+ /// Flag key: usernameMaxLength
+ /// Default value: 50
+ /// Type: int
+ ///
+ /// Optional context for the flag evaluation
+ /// Options for flag evaluation
+ /// The evaluation details containing the flag value and metadata
+ public async Task> UsernameMaxLengthDetailsAsync(EvaluationContext? evaluationContext = null, FlagEvaluationOptions? options = null)
+ {
+ return await _client.GetIntegerDetailsAsync("usernameMaxLength", 50, evaluationContext, options);
+ }
+
+
+ ///
+ /// Creates a new GeneratedClient using the default OpenFeature client
+ ///
+ /// A new GeneratedClient instance
+ public static GeneratedClient CreateClient()
+ {
+ return new GeneratedClient(Api.Instance.GetClient());
+ }
+
+ ///
+ /// Creates a new GeneratedClient using a domain-specific OpenFeature client
+ ///
+ /// The domain to get the client for
+ /// A new GeneratedClient instance
+ public static GeneratedClient CreateClient(string domain)
+ {
+ return new GeneratedClient(Api.Instance.GetClient(domain));
+ }
+
+ ///
+ /// Creates a new GeneratedClient using a domain-specific OpenFeature client with context
+ ///
+ /// The domain to get the client for
+ /// Default context to use for evaluations
+ /// A new GeneratedClient instance
+ public static GeneratedClient CreateClient(string domain, EvaluationContext? evaluationContext = null)
+ {
+ return new GeneratedClient(Api.Instance.GetClient(domain));
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/csharp-integration/test-compilation.sh b/test/csharp-integration/test-compilation.sh
new file mode 100755
index 0000000..c1fd4fe
--- /dev/null
+++ b/test/csharp-integration/test-compilation.sh
@@ -0,0 +1,34 @@
+#!/bin/bash
+set -e
+
+# Script to test if the generated C# code compiles correctly
+SCRIPT_DIR=$(dirname "$0")
+CLI_ROOT=$(realpath "$SCRIPT_DIR/../..")
+OUTPUT_DIR=$(realpath "$SCRIPT_DIR")
+
+echo "=== Building OpenFeature CLI ==="
+cd "$CLI_ROOT"
+go build
+
+echo "=== Generating C# client ==="
+./cli generate csharp --manifest="$CLI_ROOT/sample/sample_manifest.json" --output="$OUTPUT_DIR/expected" --namespace="TestNamespace"
+
+if [ ! -f "$OUTPUT_DIR/expected/OpenFeature.cs" ]; then
+ echo "Error: OpenFeature.cs was not generated"
+ exit 1
+fi
+
+echo "=== Building Docker image to compile C# code ==="
+cd "$OUTPUT_DIR"
+docker build -t openfeature-csharp-test .
+
+echo "=== Testing C# compilation and execution ==="
+docker run --rm openfeature-csharp-test
+
+if [ $? -eq 0 ]; then
+ echo "=== Success: C# code compiles and executes correctly ==="
+ exit 0
+else
+ echo "=== Error: C# code fails to compile or execute ==="
+ exit 1
+fi
\ No newline at end of file