diff --git a/.github/component_owners.yml b/.github/component_owners.yml
index eaed7942..935499fe 100644
--- a/.github/component_owners.yml
+++ b/.github/component_owners.yml
@@ -20,6 +20,8 @@ components:
- toddbaert
src/OpenFeature.Contrib.Providers.Statsig:
- jenshenneberg
+ src/OpenFeature.Contrib.Providers.Flipt:
+ - dmitryrogov
# test/
test/OpenFeature.Contrib.Hooks.Otel.Test:
@@ -41,6 +43,8 @@ components:
- toddbaert
test/src/OpenFeature.Contrib.Providers.Statsig.Test:
- jenshenneberg
+ test/src/OpenFeature.Contrib.Providers.Flipt.Test:
+ - dmitryrogov
ignored-authors:
- renovate-bot
diff --git a/DotnetSdkContrib.sln b/DotnetSdkContrib.sln
index 2c8566d1..d244f940 100644
--- a/DotnetSdkContrib.sln
+++ b/DotnetSdkContrib.sln
@@ -37,6 +37,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Provide
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest", "test\OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest\OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest.csproj", "{B8C5376B-BAFE-48B8-ABC1-111A93C033F2}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Providers.Flipt", "src\OpenFeature.Contrib.Providers.Flipt\OpenFeature.Contrib.Providers.Flipt.csproj", "{9649C012-F66B-46BB-A2C6-A3E814F4484E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Providers.Flipt.Test", "test\OpenFeature.Contrib.Providers.Flipt.Test\OpenFeature.Contrib.Providers.Flipt.Test.csproj", "{D8CE4357-82B4-4176-9273-B1B76EF1D3C9}"
+EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Providers.Statsig", "src\OpenFeature.Contrib.Providers.Statsig\OpenFeature.Contrib.Providers.Statsig.csproj", "{4C7C0E2D-6ECC-4D17-BC5D-18F6BA6F872A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Providers.Statsig.Test", "test\OpenFeature.Contrib.Providers.Statsig.Test\OpenFeature.Contrib.Providers.Statsig.Test.csproj", "{F3080350-B0AB-4D59-B416-50CC38C99087}"
@@ -107,6 +111,14 @@ Global
{B8C5376B-BAFE-48B8-ABC1-111A93C033F2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B8C5376B-BAFE-48B8-ABC1-111A93C033F2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B8C5376B-BAFE-48B8-ABC1-111A93C033F2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9649C012-F66B-46BB-A2C6-A3E814F4484E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9649C012-F66B-46BB-A2C6-A3E814F4484E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9649C012-F66B-46BB-A2C6-A3E814F4484E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9649C012-F66B-46BB-A2C6-A3E814F4484E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D8CE4357-82B4-4176-9273-B1B76EF1D3C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D8CE4357-82B4-4176-9273-B1B76EF1D3C9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D8CE4357-82B4-4176-9273-B1B76EF1D3C9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D8CE4357-82B4-4176-9273-B1B76EF1D3C9}.Release|Any CPU.Build.0 = Release|Any CPU
{4C7C0E2D-6ECC-4D17-BC5D-18F6BA6F872A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4C7C0E2D-6ECC-4D17-BC5D-18F6BA6F872A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4C7C0E2D-6ECC-4D17-BC5D-18F6BA6F872A}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -135,6 +147,8 @@ Global
{4A2C6E0F-8A23-454F-8019-AE3DD91AA193} = {B6D3230B-5E4D-4FF1-868E-2F4E325C84FE}
{2ACD9150-A8F4-450E-B49A-C628895992BF} = {B6D3230B-5E4D-4FF1-868E-2F4E325C84FE}
{B8C5376B-BAFE-48B8-ABC1-111A93C033F2} = {B6D3230B-5E4D-4FF1-868E-2F4E325C84FE}
+ {9649C012-F66B-46BB-A2C6-A3E814F4484E} = {0E563821-BD08-4B7F-BF9D-395CAD80F026}
+ {D8CE4357-82B4-4176-9273-B1B76EF1D3C9} = {B6D3230B-5E4D-4FF1-868E-2F4E325C84FE}
{4C7C0E2D-6ECC-4D17-BC5D-18F6BA6F872A} = {0E563821-BD08-4B7F-BF9D-395CAD80F026}
{F3080350-B0AB-4D59-B416-50CC38C99087} = {B6D3230B-5E4D-4FF1-868E-2F4E325C84FE}
EndGlobalSection
diff --git a/release-please-config.json b/release-please-config.json
index e13f0342..4c338592 100644
--- a/release-please-config.json
+++ b/release-please-config.json
@@ -71,6 +71,16 @@
"extra-files": [
"OpenFeature.Contrib.Providers.Statsig.csproj"
]
+ },
+ "src/OpenFeature.Contrib.Providers.Flipt": {
+ "package-name": "OpenFeature.Contrib.Providers.Flipt",
+ "release-type": "simple",
+ "bump-minor-pre-major": true,
+ "bump-patch-for-minor-pre-major": true,
+ "versioning": "default",
+ "extra-files": [
+ "OpenFeature.Contrib.Providers.Flipt.csproj"
+ ]
}
},
"changelog-sections": [
diff --git a/src/OpenFeature.Contrib.Providers.Flipt/AttachmentParser.cs b/src/OpenFeature.Contrib.Providers.Flipt/AttachmentParser.cs
new file mode 100644
index 00000000..0554de65
--- /dev/null
+++ b/src/OpenFeature.Contrib.Providers.Flipt/AttachmentParser.cs
@@ -0,0 +1,141 @@
+using OpenFeature.Error;
+using OpenFeature.Model;
+using System.Buffers.Text;
+using System.Collections.Generic;
+using System.Text;
+using System.Text.Json;
+
+namespace OpenFeature.Contrib.Providers.Flipt
+{
+ internal static class AttachmentParser
+ {
+ ///
+ /// Converts the JSON string representation of a number to its double-precision
+ /// floating-point number equivalent.
+ ///
+ /// Attachment.
+ /// Double-precision floating-point number result.
+ /// true if attachment was converted successfully; otherwise false.
+ public static bool TryParseDouble(string attachment, out double value)
+ {
+ if (string.IsNullOrEmpty(attachment))
+ {
+ value = default;
+ return false;
+ }
+
+ return Utf8Parser.TryParse(Encoding.UTF8.GetBytes(attachment), out value, out int _); ;
+ }
+
+ ///
+ /// Converts the JSON string representation of a number to its 32-bit signed integer equivalent.
+ ///
+ /// Attachment.
+ /// 32-bit signed integer result.
+ /// true if attachment was converted successfully; otherwise false.
+ public static bool TryParseInteger(string attachment, out int value)
+ {
+ if (string.IsNullOrEmpty(attachment))
+ {
+ value = default;
+ return false;
+ }
+
+ return Utf8Parser.TryParse(Encoding.UTF8.GetBytes(attachment), out value, out int _);
+ }
+
+ ///
+ /// Converts the JSON string.
+ ///
+ /// Attachment.
+ /// String result.
+ /// true if attachment was converted successfully; otherwise false.
+ public static bool TryParseString(string attachment, out string value)
+ {
+ if (string.IsNullOrEmpty(attachment))
+ {
+ value = default;
+ return false;
+ }
+
+ value = attachment.Trim('"');
+ return true;
+ }
+
+ ///
+ /// Attempts to parse a JSON attachment into a Value object.
+ /// It checks if the attachment is null or empty and tries to parse it using STJ.
+ /// If successful, it converts the parsed JSON element into a Value object.
+ ///
+ /// JSON string attachment.
+ /// Value result.
+ /// true if attachment was converted successfully; otherwise false.
+ public static bool TryParseJsonValue(string attachment, out Value value)
+ {
+ value = null;
+
+ if (string.IsNullOrEmpty(attachment))
+ {
+ return false;
+ }
+
+ try
+ {
+ value = ConvertJsonElementToValue(JsonDocument.Parse(attachment).RootElement);
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ private static Value ConvertJsonElementToValue(JsonElement element)
+ {
+ switch (element.ValueKind)
+ {
+ case JsonValueKind.String:
+ {
+ if (element.TryGetDateTime(out var dateTimeValue))
+ {
+ return new Value(dateTimeValue);
+ }
+
+ return new Value(element.GetString());
+ }
+ case JsonValueKind.Number:
+ {
+ return new Value(element.GetDouble());
+ }
+ case JsonValueKind.True:
+ case JsonValueKind.False:
+ return new Value(element.GetBoolean());
+ case JsonValueKind.Object:
+ {
+ var structureValues = new Dictionary();
+ foreach (JsonProperty property in element.EnumerateObject())
+ {
+ structureValues.Add(property.Name, ConvertJsonElementToValue(property.Value));
+ }
+
+ return new Value(new Structure(structureValues));
+ }
+ case JsonValueKind.Array:
+ {
+ var arrayValues = new List();
+ foreach (JsonElement arrayElement in element.EnumerateArray())
+ {
+ arrayValues.Add(ConvertJsonElementToValue(arrayElement));
+ }
+
+ return new Value(arrayValues);
+ }
+ case JsonValueKind.Null:
+ case JsonValueKind.Undefined:
+ return new Value();
+ default:
+ throw new ParseErrorException($"Invalid variant value: {element.GetRawText()}");
+ }
+ }
+ }
+}
diff --git a/src/OpenFeature.Contrib.Providers.Flipt/FliptConverter.cs b/src/OpenFeature.Contrib.Providers.Flipt/FliptConverter.cs
new file mode 100644
index 00000000..ee1ebed1
--- /dev/null
+++ b/src/OpenFeature.Contrib.Providers.Flipt/FliptConverter.cs
@@ -0,0 +1,109 @@
+using Flipt.Evaluation;
+using OpenFeature.Constant;
+using OpenFeature.Error;
+using OpenFeature.Model;
+using System.Diagnostics;
+
+namespace OpenFeature.Contrib.Providers.Flipt
+{
+ internal static class FliptConverter
+ {
+ ///
+ /// Converts an EvaluationReason value into a corresponding string representation based on predefined reasons.
+ ///
+ /// Predefined OpenFeature reason.
+ ///
+ public static string ConvertReason(EvaluationReason reason)
+ {
+ switch (reason)
+ {
+ case EvaluationReason.UnknownEvaluationReason:
+ return Reason.Unknown;
+ case EvaluationReason.FlagDisabledEvaluationReason:
+ return Reason.Disabled;
+ case EvaluationReason.MatchEvaluationReason:
+ return Reason.TargetingMatch;
+ case EvaluationReason.DefaultEvaluationReason:
+ return Reason.Default;
+ default:
+ return Reason.Default;
+ }
+ }
+
+ ///
+ /// Creates Flipt evaluation request.
+ ///
+ /// Flag key.
+ /// Evaluation context.
+ /// Provider configuration.
+ /// Flipt evaluation request.
+ /// Unable to convert context value.
+ public static EvaluationRequest CreateRequest(string flagKey, EvaluationContext context, FliptProviderConfiguration config)
+ {
+ var request = new EvaluationRequest
+ {
+ NamespaceKey = config.Namespace,
+ FlagKey = flagKey
+ };
+
+ if (Activity.Current != null)
+ {
+ request.RequestId = Activity.Current.Id;
+ }
+
+ if (context == null || context.Count == 0)
+ {
+ return request;
+ }
+
+ foreach (var item in context)
+ {
+ var key = item.Key;
+ var value = item.Value;
+
+ if (value.IsNull || value.IsList || value.IsStructure)
+ {
+ // Skip null, lists and complex objects
+ continue;
+ }
+
+ if (key == config.TargetingKey && value.IsString)
+ {
+ // Skip targeting key and add its value as EntityId to request
+ request.EntityId = value.AsString;
+ continue;
+ }
+
+ if (key == config.RequestIdKey && value.IsString)
+ {
+ // Skip request id key and add its value as RequestId to request
+ request.RequestId = value.AsString;
+ continue;
+ }
+
+ if (value.IsString)
+ {
+ request.Context.Add(key, value.AsString);
+ }
+ else if (value.IsBoolean)
+ {
+ request.Context.Add(key, value.AsBoolean.ToString());
+ }
+ else if (value.IsNumber)
+ {
+ request.Context.Add(key, value.AsDouble.ToString());
+ }
+ else if (value.IsDateTime)
+ {
+ request.Context.Add(key, $"{value.AsDateTime.Value:o}");
+ }
+ else
+ {
+ throw new InvalidContextException($"Unable to convert context value with key: {key}.");
+ }
+ }
+
+ return request;
+ }
+ }
+}
diff --git a/src/OpenFeature.Contrib.Providers.Flipt/FliptProvider.cs b/src/OpenFeature.Contrib.Providers.Flipt/FliptProvider.cs
new file mode 100644
index 00000000..43c6d652
--- /dev/null
+++ b/src/OpenFeature.Contrib.Providers.Flipt/FliptProvider.cs
@@ -0,0 +1,155 @@
+using Grpc.Core;
+using Grpc.Net.Client;
+using OpenFeature.Error;
+using OpenFeature.Model;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using static Flipt.Evaluation.EvaluationService;
+using EvaluationRequest = Flipt.Evaluation.EvaluationRequest;
+using Metadata = OpenFeature.Model.Metadata;
+
+namespace OpenFeature.Contrib.Providers.Flipt
+{
+ ///
+ /// Flipt feature provider.
+ ///
+ public sealed class FliptProvider : FeatureProvider
+ {
+ private static readonly Metadata Metadata = new Metadata("Flipt");
+ private readonly EvaluationServiceClient _client;
+ private readonly FliptProviderConfiguration _configuration;
+
+ ///
+ /// Creates new instance of
+ ///
+ /// Flipt provider options.
+ public FliptProvider(FliptProviderConfiguration configuration)
+ {
+ _configuration = configuration;
+ var channel = GrpcChannel.ForAddress(configuration.ServiceUri);
+ _client = new EvaluationServiceClient(channel);
+ }
+
+ ///
+ /// Creates new instance of
+ ///
+ ///
+ /// Flipt provider options.
+ internal FliptProvider(EvaluationServiceClient evaluationServiceClient, FliptProviderConfiguration configuration)
+ {
+ _configuration = configuration;
+ _client = evaluationServiceClient;
+ }
+
+ ///
+ public override Metadata GetMetadata()
+ {
+ return Metadata;
+ }
+
+ ///
+ public override Task> ResolveBooleanValue(string flagKey, bool defaultValue,
+ EvaluationContext context = null)
+ {
+ return _configuration.UseBooleanEvaluation
+ ? ResolveBooleanAsync(flagKey, context)
+ : ResolveVariantAsync(flagKey, defaultValue, context, bool.TryParse);
+ }
+
+ ///
+ public override Task> ResolveDoubleValue(string flagKey, double defaultValue,
+ EvaluationContext context = null)
+ {
+ return ResolveVariantAsync(flagKey, defaultValue, context, AttachmentParser.TryParseDouble);
+ }
+
+ ///
+ public override Task> ResolveIntegerValue(string flagKey, int defaultValue,
+ EvaluationContext context = null)
+ {
+ return ResolveVariantAsync(flagKey, defaultValue, context, AttachmentParser.TryParseInteger);
+ }
+
+ ///
+ public override Task> ResolveStringValue(string flagKey, string defaultValue,
+ EvaluationContext context = null)
+ {
+ return ResolveVariantAsync(flagKey, defaultValue, context, AttachmentParser.TryParseString);
+ }
+
+ ///
+ public override Task> ResolveStructureValue(string flagKey, Value defaultValue,
+ EvaluationContext context = null)
+ {
+ return ResolveVariantAsync(flagKey, defaultValue, context, AttachmentParser.TryParseJsonValue);
+ }
+
+ private async Task> ResolveBooleanAsync(string flagKey, EvaluationContext context)
+ {
+ var request = FliptConverter.CreateRequest(flagKey, context, _configuration);
+ var response = await SendRequestAsync(_client.BooleanAsync, request);
+ return new ResolutionDetails(
+ response.FlagKey,
+ response.Enabled,
+ reason: FliptConverter.ConvertReason(response.Reason));
+ }
+
+ internal async Task> ResolveVariantAsync(string flagKey, T defaultValue,
+ EvaluationContext context, TryParseDelegate tryParse)
+ {
+ var request = FliptConverter.CreateRequest(flagKey, context, _configuration);
+ var response = await SendRequestAsync(_client.VariantAsync, request);
+
+ if (!response.Match)
+ return new ResolutionDetails(
+ response.FlagKey,
+ defaultValue,
+ reason: FliptConverter.ConvertReason(response.Reason));
+
+ if (tryParse(response.VariantAttachment, out var value))
+ return new ResolutionDetails(
+ response.FlagKey,
+ value,
+ variant: response.VariantKey,
+ reason: FliptConverter.ConvertReason(response.Reason));
+ throw new ParseErrorException(
+ $"Can't convert value \"{response.VariantAttachment}\" to \"{typeof(T).Name}\" type");
+ }
+
+ internal static async Task SendRequestAsync(SendRequestDelegate sendRequest,
+ EvaluationRequest request)
+ {
+ try
+ {
+ return await sendRequest(request);
+ }
+ catch (RpcException ex)
+ {
+ if (ex.StatusCode == StatusCode.NotFound)
+ {
+ throw new FlagNotFoundException(ex.Status.Detail);
+ }
+
+ if (ex.StatusCode == StatusCode.InvalidArgument)
+ {
+ throw new InvalidContextException(ex.Status.Detail);
+ }
+
+ throw new GeneralException(ex.Message, ex);
+ }
+ catch (Exception ex)
+ {
+ throw new GeneralException(ex.Message, ex);
+ }
+ }
+
+ internal delegate bool TryParseDelegate(string value, out T result);
+
+ internal delegate AsyncUnaryCall SendRequestDelegate(
+ EvaluationRequest request,
+ Grpc.Core.Metadata headers = null,
+ DateTime? deadline = null,
+ CancellationToken cancellationToken = default);
+ }
+}
\ No newline at end of file
diff --git a/src/OpenFeature.Contrib.Providers.Flipt/FliptProviderConfiguration.cs b/src/OpenFeature.Contrib.Providers.Flipt/FliptProviderConfiguration.cs
new file mode 100644
index 00000000..021d908c
--- /dev/null
+++ b/src/OpenFeature.Contrib.Providers.Flipt/FliptProviderConfiguration.cs
@@ -0,0 +1,38 @@
+using System;
+
+namespace OpenFeature.Contrib.Providers.Flipt
+{
+ ///
+ /// Flipt provider configuration
+ ///
+ public class FliptProviderConfiguration
+ {
+ ///
+ /// Flipt service address
+ ///
+ public Uri ServiceUri { get; set; }
+
+ ///
+ /// Namespace
+ ///
+ ///
+ public string Namespace { get; set; }
+
+ ///
+ /// Context key whose value will be used as EntityId
+ ///
+ ///
+ public string TargetingKey { get; set; }
+
+ ///
+ /// Context key whose value will be used as RequestId
+ ///
+ public string RequestIdKey { get; set; }
+
+ ///
+ /// Determines whether to use Boolean Evaluation or Variant Evaluation API
+ ///
+ ///
+ public bool UseBooleanEvaluation { get; set; }
+ }
+}
diff --git a/src/OpenFeature.Contrib.Providers.Flipt/OpenFeature.Contrib.Providers.Flipt.csproj b/src/OpenFeature.Contrib.Providers.Flipt/OpenFeature.Contrib.Providers.Flipt.csproj
new file mode 100644
index 00000000..0429805b
--- /dev/null
+++ b/src/OpenFeature.Contrib.Providers.Flipt/OpenFeature.Contrib.Providers.Flipt.csproj
@@ -0,0 +1,28 @@
+
+
+
+ OpenFeature.Contrib.Providers.Flipt
+ 0.0.1
+ $(VersionNumber)
+ $(VersionNumber)
+ $(VersionNumber)
+ Flipt provider for .NET
+ README.md
+ Rogov Dmitry
+
+
+
+
+
+
+
+
+
+ <_Parameter1>$(MSBuildProjectName).Test
+
+
+
+
+
+
+
diff --git a/src/OpenFeature.Contrib.Providers.Flipt/README.md b/src/OpenFeature.Contrib.Providers.Flipt/README.md
new file mode 100644
index 00000000..0dec45af
--- /dev/null
+++ b/src/OpenFeature.Contrib.Providers.Flipt/README.md
@@ -0,0 +1,195 @@
+# Flipt .NET Provider
+
+An OpenFeature Provider which enables the use of the Flipt Server-Side SDK.
+
+# .NET SDK usage
+
+## Install dependencies
+
+We will first install the **flipt provider**.
+
+#### .NET CLI
+```shell
+dotnet add package OpenFeature.Contrib.Providers.Flipt
+```
+#### Package Manager
+
+```shell
+NuGet\Install-Package OpenFeature.Contrib.Providers.Flipt
+```
+#### Package Reference
+
+```xml
+
+```
+#### Paket CLI
+
+```shell
+paket add OpenFeature.Contrib.Providers.Flagd
+```
+
+
+## Using the Flipt Provider with the OpenFeature SDK
+
+Using the Flip provider assumes that you have installed and running Flipt service. Instructions on how to do this can be found in the [official documentation](https://docs.flipt.io/self-hosted/overview)
+
+The following example shows how to use the Flipt provider with the OpenFeature SDK.
+
+*appsettings.json*
+```json
+{
+ "OpenFeature": {
+ "Provider": {
+ "Flipt": {
+ "ServiceUri": "http://localhost:9000",
+ "Namespace": "default",
+ "TargetingKey": "UserId",
+ "RequestIdKey": "RequestId",
+ "UseBooleanEvaluation": false
+ }
+ }
+ }
+}
+```
+
+*Program.cs*
+
+```csharp
+using OpenFeature.Contrib.Providers.Flipt;
+
+var builder = WebApplication.CreateBuilder(args);
+
+// Get provider config from configuration
+var config = builder.Configuration.GetSection("OpenFeature:Provider:Flipt").Get();
+
+/* Or create config directly:
+var config = new FliptProviderConfiguration
+{
+ ServiceUri = new Uri("http://localhost:9000"),
+ Namespace = "default",
+ TargetingKey = "UserId",
+ RequestIdKey = "RequestId",
+ UseBooleanEvaluation = false
+};*/
+
+// Create an instance of Flipt provider
+var fliptProvider = new FliptProvider(config);
+
+// Set the Flipt Provider as the provider for the OpenFeature SDK
+await OpenFeature.Api.Instance.SetProviderAsync(fliptProvider);
+
+// Create OpenFeature client with current provider
+var client = OpenFeature.Api.Instance.GetClient("my-app");
+
+// Resolve boolean flag value
+var isTestFlagEnabled = await client.GetBooleanValue("test-flag", false, null);
+
+System.Console.WriteLine($"Test flag enabled: {isTestFlagEnabled}");
+```
+
+## Boolean flag evaluation
+To perform Boolean flag evaluation, such as `GetBooleanValue` or `GetBooleanDetail` you can utilize [Boolean Evaluation](https://docs.flipt.io/reference/evaluation/boolean-evaluation) or [Variant Evaluation](https://docs.flipt.io/reference/evaluation/variant-evaluation) APIs.
+
+The preferred method can be selected by configuring the `FliptProviderConfiguration.UseBooleanEvaluation` parameter.
+
+Note that when using the Variant Evaluation API, the value of the variant **must** be a boolean, represented as either `"true"` or `"false"`.
+
+## Using EvaluationContext
+
+Flipt has the concept of [Context](https://docs.flipt.io/concepts#context) where you can evaluate a flag based on properties. The OpenFeature SDK has the concept of an [EvaluationContext](https://openfeature.dev/specification/sections/evaluation-context) which is a dictionary of string keys and values. The Flipt provider will map the EvaluationContext to a Flipt Context.
+
+Since Flipt Context is a simple array of objects like `[{"key": "value"}]`, the provider only supports primitive types such as `number`, `string`, `date-time` and `boolean`. Complex types like `list` or `structure` will be ingnored.
+
+For example context:
+```csharp
+var context = EvaluationContext.Builder()
+ .Set("role", "user")
+ .Set("age", 22)
+ .Set("privileged", true)
+ .Set("created", new DateTime(2011, 8, 23))
+ .Build();
+```
+Transforms into:
+```json
+{
+ "context": [
+ {
+ "key": "role",
+ "value": "user"
+ },
+ {
+ "value": "age",
+ "key": "22"
+ },
+ {
+ "value": "privileged",
+ "key": "true"
+ },
+ {
+ "key": "created",
+ "value": "2011-08-23T00:00:00.0000000"
+ }
+ ]
+}
+```
+
+### Set entityId
+To pass the [`entityId`](https://docs.flipt.io/concepts#entities) you need to specify a key `FliptProviderConfiguration.TargetingKey`. The value in `EvaluationContext` assigned to this key will be used as the `entityId` request parameter and will not be mapped into context. The value **must** be of string type.
+
+For example:
+```csharp
+var config = new FliptProviderConfiguration
+{
+ ServiceUri = new Uri("http://localhost:9000"),
+ Namespace = "default",
+ TargetingKey = "UserId"
+};
+var context = EvaluationContext
+ .Builder()
+ .Set(config.TargetingKey, "some-user-id")
+ .Build();
+var isTestFlagEnabled = await client.GetBooleanValue("test-flag", false, context);
+```
+Request:
+```json
+{
+ "context": [],
+ "entity_id": "some-user-id",
+ "flag_key": "test-flag",
+ "namespace_key": "default",
+ "request_id": ""
+}
+```
+
+### Set requestId
+To pass the `requestId` you need to specify a key `FliptProviderConfiguration.RequestIdKey`. The value in `EvaluationContext` assigned to this key will be used as the `entityId` request parameter and will not be mapped into context. The value **must** be of string type.
+
+If the key is not specified in the configuration, or the context does not contain a value for this key, then `Activity.Current.Id` will be used if it present.
+
+For example:
+```csharp
+var config = new FliptProviderConfiguration
+{
+ ServiceUri = new Uri("http://localhost:9000"),
+ Namespace = "default",
+ TargetingKey = "UserId"
+};
+var context = EvaluationContext
+ .Builder()
+ .Set(config.TargetingKey, "some-user-id");
+var isTestFlagEnabled = await client.GetBooleanValue("test-flag", false, context);
+```
+Request:
+```json
+{
+ "context": [],
+ "entity_id": "some-user-id",
+ "flag_key": "test-flag",
+ "namespace_key": "default",
+ "request_id": ""
+}
+```
+
+
+
+
diff --git a/src/OpenFeature.Contrib.Providers.Flipt/version.txt b/src/OpenFeature.Contrib.Providers.Flipt/version.txt
new file mode 100644
index 00000000..8acdd82b
--- /dev/null
+++ b/src/OpenFeature.Contrib.Providers.Flipt/version.txt
@@ -0,0 +1 @@
+0.0.1
diff --git a/test/OpenFeature.Contrib.Providers.Flipt.Test/AttachmentParserTest.cs b/test/OpenFeature.Contrib.Providers.Flipt.Test/AttachmentParserTest.cs
new file mode 100644
index 00000000..95d2cf2b
--- /dev/null
+++ b/test/OpenFeature.Contrib.Providers.Flipt.Test/AttachmentParserTest.cs
@@ -0,0 +1,236 @@
+using AutoFixture;
+using FluentAssertions;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
+using Xunit;
+
+namespace OpenFeature.Contrib.Providers.Flipt.Test
+{
+ public class AttachmentParserTest
+ {
+ private readonly IFixture _fixture = new Fixture();
+
+ ///
+ /// DateTime test data
+ ///
+ /// Attachment | Expected value
+ public static IEnumerable