├── .gitignore ├── CHANGES.txt ├── CONTRIBUTORS-GUIDE.md ├── LICENSE ├── ProviderTests ├── CustomLogger.cs ├── ProviderTests.cs ├── ProviderTests.csproj ├── Usings.cs └── split.yaml ├── README.md ├── SplitOpenFeatureProvider.sln └── SplitOpenFeatureProvider ├── Provider.cs └── SplitOpenFeatureProvider.csproj /.gitignore: -------------------------------------------------------------------------------- 1 | .vs/ 2 | bin/ 3 | obj/ 4 | -------------------------------------------------------------------------------- /CHANGES.txt: -------------------------------------------------------------------------------- 1 | 1.0.0 2 | - First release. Up to date with OpenFeature .NET SDK 1.0.1 3 | -------------------------------------------------------------------------------- /CONTRIBUTORS-GUIDE.md: -------------------------------------------------------------------------------- 1 | # Contributing to the Split OpenFeature Provider 2 | 3 | The Split Provider is an open source project and we welcome feedback and contribution. The information below describes how to build the project with your changes, run the tests, and send the Pull Request(PR). 4 | 5 | ## Development 6 | 7 | ### Development process 8 | 9 | 1. Fork the repository and create a topic branch from `development` branch. Please use a descriptive name for your branch. 10 | 2. While developing, use descriptive messages in your commits. Avoid short or meaningless sentences like "fix bug". 11 | 3. Make sure to add tests for both positive and negative cases. 12 | 4. Run the build script and make sure it runs with no errors. 13 | 5. Run all tests and make sure there are no failures. 14 | 6. `git push` your changes to GitHub within your topic branch. 15 | 7. Open a Pull Request(PR) from your forked repo and into the `development` branch of the original repository. 16 | 8. When creating your PR, please fill out all the fields of the PR template, as applicable, for the project. 17 | 9. Check for conflicts once the pull request is created to make sure your PR can be merged cleanly into `development`. 18 | 10. Keep an eye out for any feedback or comments from the Split team. 19 | 20 | ### Building the Split Provider 21 | - `mvn clean build` 22 | 23 | ### Running tests 24 | - `mvn clean install` 25 | 26 | # Contact 27 | 28 | If you have any other questions or need to contact us directly in a private manner send us a note at sdks@split.io 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2022 Split Software, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /ProviderTests/CustomLogger.cs: -------------------------------------------------------------------------------- 1 | using Splitio.Services.Logger; 2 | 3 | namespace ProviderTests 4 | { 5 | public class CustomLogger : ISplitLogger 6 | { 7 | public void Debug(string message, Exception exception) 8 | { 9 | Console.WriteLine($"DEBUG: {message}: {exception}"); 10 | } 11 | 12 | public void Debug(string message) { 13 | Console.WriteLine($"DEBUG: {message}"); 14 | } 15 | 16 | public void Error(string message, Exception exception) { 17 | Console.WriteLine($"ERROR: {message}: {exception}"); 18 | } 19 | 20 | public void Error(string message) { 21 | Console.WriteLine($"DEBUG: {message}"); 22 | } 23 | 24 | public void Info(string message, Exception exception) { 25 | Console.WriteLine($"INFO: {message}: {exception}"); 26 | } 27 | 28 | public void Info(string message) { 29 | Console.WriteLine($"INFO: {message}"); 30 | } 31 | 32 | public void Trace(string message, Exception exception) { 33 | Console.WriteLine($"TRACE: {message}: {exception}"); 34 | } 35 | 36 | public void Trace(string message) { 37 | Console.WriteLine($"TRACE: {message}"); 38 | } 39 | 40 | public void Warn(string message, Exception exception) { 41 | Console.WriteLine($"WARN: {message}: {exception}"); 42 | } 43 | 44 | public void Warn(string message) { 45 | Console.WriteLine($"WARN: {message}"); 46 | } 47 | 48 | public bool IsDebugEnabled { get { return true; } } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /ProviderTests/ProviderTests.cs: -------------------------------------------------------------------------------- 1 | using OpenFeature; 2 | using OpenFeature.Constant; 3 | using OpenFeature.Model; 4 | using Splitio.Services.Client.Classes; 5 | using Splitio.OpenFeature; 6 | 7 | namespace ProviderTests; 8 | 9 | [TestClass] 10 | public class ProviderTests 11 | { 12 | readonly FeatureClient client; 13 | 14 | public ProviderTests() 15 | { 16 | // Create the Split client 17 | var config = new ConfigurationOptions 18 | { 19 | LocalhostFilePath = "../../../split.yaml", 20 | Logger = new CustomLogger() 21 | }; 22 | var factory = new SplitFactory("localhost", config); 23 | var sdk = factory.Client(); 24 | try 25 | { 26 | sdk.BlockUntilReady(10000); 27 | } 28 | catch (Exception ex) 29 | { 30 | Console.WriteLine($"Exception initializing Split client! {ex}"); 31 | throw; 32 | } 33 | 34 | // Sets the provider used by the client 35 | OpenFeature.Api.Instance.SetProvider(new Provider(sdk)); 36 | 37 | // Gets a instance of the feature flag client 38 | client = OpenFeature.Api.Instance.GetClient(); 39 | var context = EvaluationContext.Builder().Set("targetingKey", "key").Build(); 40 | client.SetContext(context); 41 | } 42 | 43 | [TestMethod] 44 | public async Task UseDefaultTest() 45 | { 46 | String flagName = "random-non-existent-feature"; 47 | 48 | var result = await client.GetBooleanValue(flagName, false); 49 | Assert.IsFalse(result); 50 | result = await client.GetBooleanValue(flagName, true); 51 | Assert.IsTrue(result); 52 | 53 | String defaultString = "blah"; 54 | var resultString = await client.GetStringValue(flagName, defaultString); 55 | Assert.AreEqual(defaultString, resultString); 56 | 57 | int defaultInt = 100; 58 | var resultInt = await client.GetIntegerValue(flagName, defaultInt); 59 | Assert.AreEqual(defaultInt, resultInt); 60 | 61 | Structure defaultStructure = Structure.Builder().Set("foo", new Value("bar")).Build(); 62 | Value resultStructure = await client.GetObjectValue(flagName, new Value(defaultStructure)); 63 | Assert.IsTrue(StructuresMatch(defaultStructure, resultStructure.AsStructure)); 64 | } 65 | 66 | [TestMethod] 67 | public async Task MissingTargetingKeyTest() 68 | { 69 | client.SetContext(EvaluationContext.Builder().Build()); 70 | FlagEvaluationDetails details = await client.GetBooleanDetails("non-existent-feature", false); 71 | Assert.IsFalse(details.Value); 72 | Assert.AreEqual(ErrorType.TargetingKeyMissing, details.ErrorType); 73 | } 74 | 75 | [TestMethod] 76 | public async Task GetControlVariantNonExistentSplit() 77 | { 78 | FlagEvaluationDetails details = await client.GetBooleanDetails("non-existent-feature", false); 79 | Assert.IsFalse(details.Value); 80 | Assert.AreEqual("control", details.Variant); 81 | Assert.AreEqual(ErrorType.FlagNotFound, details.ErrorType); 82 | } 83 | 84 | [TestMethod] 85 | public async Task GetBooleanSplitTest() 86 | { 87 | var result = await client.GetBooleanValue("some_other_feature", true); 88 | Assert.IsFalse(result); 89 | } 90 | 91 | [TestMethod] 92 | public async Task GetBooleanSplitWithKeyTest() 93 | { 94 | var result = await client.GetBooleanValue("my_feature", false); 95 | Assert.IsTrue(result); 96 | 97 | var context = EvaluationContext.Builder().Set("targetingKey", "randomKey").Build(); 98 | result = await client.GetBooleanValue("my_feature", true, context); 99 | Assert.IsFalse(result); 100 | } 101 | 102 | [TestMethod] 103 | public async Task GetStringSplitTest() 104 | { 105 | var result = await client.GetStringValue("some_other_feature", "on"); 106 | Assert.AreEqual("off", result); 107 | } 108 | 109 | [TestMethod] 110 | public async Task GetIntegerSplitTest() 111 | { 112 | var result = await client.GetIntegerValue("int_feature", 0); 113 | Assert.AreEqual(32, result); 114 | } 115 | 116 | [TestMethod] 117 | public async Task GetObjectSplitTest() 118 | { 119 | var result = await client.GetObjectValue("obj_feature", new Value()); 120 | Structure expectedValue = Structure.Builder().Set("key", new Value("value")).Build(); 121 | Assert.IsTrue(StructuresMatch(expectedValue, result.AsStructure)); 122 | } 123 | 124 | [TestMethod] 125 | public async Task GetDoubleSplitTest() 126 | { 127 | var result = await client.GetDoubleValue("int_feature", 0D); 128 | Assert.AreEqual(32D, result); 129 | } 130 | 131 | [TestMethod] 132 | public async Task GetBooleanDetailsTest() 133 | { 134 | var details = await client.GetBooleanDetails("some_other_feature", true); 135 | Assert.AreEqual("some_other_feature", details.FlagKey); 136 | Assert.AreEqual(Reason.TargetingMatch, details.Reason); 137 | Assert.IsFalse(details.Value); 138 | // the flag has a treatment of "off", this is returned as a value of false but the variant is still "off" 139 | Assert.AreEqual("off", details.Variant); 140 | Assert.AreEqual(ErrorType.None, details.ErrorType); 141 | } 142 | 143 | [TestMethod] 144 | public async Task GetIntegerDetailsTest() 145 | { 146 | var details = await client.GetIntegerDetails("int_feature", 0); 147 | Assert.AreEqual("int_feature", details.FlagKey); 148 | Assert.AreEqual(Reason.TargetingMatch, details.Reason); 149 | Assert.AreEqual(32, details.Value); 150 | // the flag has a treatment of "off", this is returned as a value of false but the variant is still "off" 151 | Assert.AreEqual("32", details.Variant); 152 | Assert.AreEqual(ErrorType.None, details.ErrorType); 153 | } 154 | 155 | [TestMethod] 156 | public async Task GetStringDetailsTest() 157 | { 158 | var details = await client.GetStringDetails("some_other_feature", "blah"); 159 | Assert.AreEqual("some_other_feature", details.FlagKey); 160 | Assert.AreEqual(Reason.TargetingMatch, details.Reason); 161 | Assert.AreEqual("off", details.Value); 162 | // the flag has a treatment of "off", this is returned as a value of false but the variant is still "off" 163 | Assert.AreEqual("off", details.Variant); 164 | Assert.AreEqual(ErrorType.None, details.ErrorType); 165 | } 166 | 167 | [TestMethod] 168 | public async Task GetDoubleDetailsTest() 169 | { 170 | var details = await client.GetDoubleDetails("int_feature", 0D); 171 | Assert.AreEqual("int_feature", details.FlagKey); 172 | Assert.AreEqual(Reason.TargetingMatch, details.Reason); 173 | Assert.AreEqual(32D, details.Value); 174 | // the flag has a treatment of "off", this is returned as a value of false but the variant is still "off" 175 | Assert.AreEqual("32", details.Variant); 176 | Assert.AreEqual(ErrorType.None, details.ErrorType); 177 | } 178 | 179 | [TestMethod] 180 | public async Task GetObjectDetailsTest() 181 | { 182 | var details = await client.GetObjectDetails("obj_feature", new Value()); 183 | Assert.AreEqual("obj_feature", details.FlagKey); 184 | Assert.AreEqual(Reason.TargetingMatch, details.Reason); 185 | Structure expected = Structure.Builder().Set("key", new Value("value")).Build(); 186 | Assert.IsTrue(StructuresMatch(expected, details.Value.AsStructure)); 187 | // the flag's treatment is stored as a string, and the variant is that raw string 188 | Assert.AreEqual("{\"key\": \"value\"}", details.Variant); 189 | Assert.AreEqual(ErrorType.None, details.ErrorType); 190 | } 191 | 192 | [TestMethod] 193 | public async Task GetBooleanFailTest() 194 | { 195 | // attempt to fetch an object treatment as a Boolean. Should result in the default 196 | var value = await client.GetBooleanValue("obj_feature", false); 197 | Assert.IsFalse(value); 198 | 199 | var details = await client.GetBooleanDetails("obj_feature", false); 200 | Assert.IsFalse(details.Value); 201 | Assert.AreEqual(ErrorType.ParseError, details.ErrorType); 202 | Assert.AreEqual(Reason.Error, details.Reason); 203 | } 204 | 205 | [TestMethod] 206 | public async Task GetIntegerFailTest() 207 | { 208 | // attempt to fetch an object treatment as an integer. Should result in the default 209 | var value = await client.GetIntegerValue("obj_feature", 10); 210 | Assert.AreEqual(10, value); 211 | 212 | var details = await client.GetIntegerDetails("obj_feature", 10); 213 | Assert.AreEqual(10, details.Value); 214 | Assert.AreEqual(ErrorType.ParseError, details.ErrorType); 215 | Assert.AreEqual(Reason.Error, details.Reason); 216 | } 217 | 218 | [TestMethod] 219 | public async Task GetDoubleFailTest() 220 | { 221 | // attempt to fetch an object treatment as a double. Should result in the default 222 | var value = await client.GetDoubleValue("obj_feature", 10D); 223 | Assert.AreEqual(10D, value); 224 | 225 | var details = await client.GetDoubleDetails("obj_feature", 10D); 226 | Assert.AreEqual(10D, details.Value); 227 | Assert.AreEqual(ErrorType.ParseError, details.ErrorType); 228 | Assert.AreEqual(Reason.Error, details.Reason); 229 | } 230 | 231 | [TestMethod] 232 | public async Task GetObjectFailTest() 233 | { 234 | // attempt to fetch an int as an object. Should result in the default 235 | Structure defaultValue = Structure.Builder().Set("key", new Value("value")).Build(); 236 | var value = await client.GetObjectValue("int_feature", new Value(defaultValue)); 237 | Assert.IsTrue(StructuresMatch(defaultValue, value.AsStructure)); 238 | 239 | var details = await client.GetObjectDetails("int_feature", new Value(defaultValue)); 240 | Assert.IsTrue(StructuresMatch(defaultValue, details.Value.AsStructure)); 241 | Assert.AreEqual(ErrorType.ParseError, details.ErrorType); 242 | Assert.AreEqual(Reason.Error, details.Reason); 243 | } 244 | 245 | private static bool StructuresMatch(Structure s1, Structure s2) 246 | { 247 | if (s1.Count != s2.Count) 248 | { 249 | return false; 250 | } 251 | foreach (string key in s1.Keys) 252 | { 253 | var v1 = s1.GetValue(key); 254 | var v2 = s2.GetValue(key); 255 | if (v1.ToString() != v2.ToString()) 256 | { 257 | return false; 258 | } 259 | } 260 | return true; 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /ProviderTests/ProviderTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /ProviderTests/Usings.cs: -------------------------------------------------------------------------------- 1 | global using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | -------------------------------------------------------------------------------- /ProviderTests/split.yaml: -------------------------------------------------------------------------------- 1 | - my_feature: 2 | treatment: "off" 3 | - my_feature: 4 | treatment: "on" 5 | keys: "key" 6 | config: "{\"desc\" : \"this applies only to ON treatment\"}" 7 | - some_other_feature: 8 | treatment: "off" 9 | - int_feature: 10 | treatment: "32" 11 | - obj_feature: 12 | treatment: "{\"key\": \"value\"}" 13 | config: "{\"key\": \"value\"}" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Split OpenFeature Provider for .NET 2 | [![Twitter Follow](https://img.shields.io/twitter/follow/splitsoftware.svg?style=social&label=Follow&maxAge=1529000)](https://twitter.com/intent/follow?screen_name=splitsoftware) 3 | 4 | ## Overview 5 | This Provider is designed to allow the use of OpenFeature with Split, the platform for controlled rollouts, serving features to your users via the Split feature flag to manage your complete customer experience. 6 | 7 | ## Compatibility 8 | This SDK is compatible with .NET 6.0 and higher. 9 | 10 | ## Getting started 11 | Below is a simple example that describes the instantiation of the Split Provider. Please see the [OpenFeature Documentation](https://docs.openfeature.dev/docs/reference/concepts/evaluation-api) for details on how to use the OpenFeature SDK. 12 | 13 | ```c# 14 | using OpenFeature; 15 | using Splitio.OpenFeature; 16 | 17 | Api api = OpenFeature.Api.Instance; 18 | api.setProvider(new Provider("YOUR_API_KEY")); 19 | ``` 20 | 21 | If you are more familiar with Split or want access to other initialization options, you can provide a `Split Client` to the constructor. See the [Split .NET Documentation](https://help.split.io/hc/en-us/articles/360020240172--NET-SDK) for more information. 22 | ```c# 23 | using OpenFeature; 24 | using Splitio.OpenFeature; 25 | using Splitio.Services.Client.Classes 26 | 27 | Api api = OpenFeature.Api.Instance; 28 | 29 | var config = new ConfigurationOptions 30 | { 31 | Ready = 10000 32 | }; 33 | var splitClient = new SplitFactory("YOUR_API_KEY", config).Client(); 34 | api.SetProvider(new Provider(splitClient)); 35 | ``` 36 | 37 | ## Use of OpenFeature with Split 38 | After the initial setup you can use OpenFeature according to their [documentation](https://docs.openfeature.dev/docs/reference/concepts/evaluation-api/). 39 | 40 | One important note is that the Split Provider **requires a targeting key** to be set. Often times this should be set when evaluating the value of a flag by [setting an EvaluationContext](https://docs.openfeature.dev/docs/reference/concepts/evaluation-context) which contains the targeting key. An example flag evaluation is 41 | ```csharp 42 | var context = EvaluationContext.Builder().Set("targetingKey", "randomKey").Build(); 43 | var result = await client.GetBooleanValue("boolFlag", false, context); 44 | ``` 45 | If the same targeting key is used repeatedly, the evaluation context may be set at the client level 46 | ```csharp 47 | var context = EvaluationContext.Builder().Set("targetingKey", "randomKey").Build(); 48 | client.SetContext(context) 49 | ``` 50 | or at the OpenFeatureAPI level 51 | ```csharp 52 | var context = EvaluationContext.Builder().Set("targetingKey", "randomKey").Build(); 53 | api.setEvaluationContext(context) 54 | ```` 55 | If the context was set at the client or api level, it is not required to provide it during flag evaluation. 56 | 57 | ## Submitting issues 58 | 59 | The Split team monitors all issues submitted to this [issue tracker](https://github.com/splitio/split-openfeature-provider-dotnet/issues). We encourage you to use this issue tracker to submit any bug reports, feedback, and feature enhancements. We'll do our best to respond in a timely manner. 60 | 61 | ## Contributing 62 | Please see [Contributors Guide](CONTRIBUTORS-GUIDE.md) to find all you need to submit a Pull Request (PR). 63 | 64 | ## License 65 | Licensed under the Apache License, Version 2.0. See: [Apache License](http://www.apache.org/licenses/). 66 | 67 | ## About Split 68 | 69 | Split is the leading Feature Delivery Platform for engineering teams that want to confidently deploy features as fast as they can develop them. Split’s fine-grained management, real-time monitoring, and data-driven experimentation ensure that new features will improve the customer experience without breaking or degrading performance. Companies like Twilio, Salesforce, GoDaddy and WePay trust Split to power their feature delivery. 70 | 71 | To learn more about Split, contact hello@split.io, or get started with feature flags for free at https://www.split.io/signup. 72 | 73 | Split has built and maintains SDKs for: 74 | 75 | * Java [Github](https://github.com/splitio/java-client) [Docs](https://help.split.io/hc/en-us/articles/360020405151-Java-SDK) 76 | * Javascript [Github](https://github.com/splitio/javascript-client) [Docs](https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK) 77 | * Node [Github](https://github.com/splitio/javascript-client) [Docs](https://help.split.io/hc/en-us/articles/360020564931-Node-js-SDK) 78 | * .NET [Github](https://github.com/splitio/dotnet-client) [Docs](https://help.split.io/hc/en-us/articles/360020240172--NET-SDK) 79 | * Ruby [Github](https://github.com/splitio/ruby-client) [Docs](https://help.split.io/hc/en-us/articles/360020673251-Ruby-SDK) 80 | * PHP [Github](https://github.com/splitio/php-client) [Docs](https://help.split.io/hc/en-us/articles/360020350372-PHP-SDK) 81 | * Python [Github](https://github.com/splitio/python-client) [Docs](https://help.split.io/hc/en-us/articles/360020359652-Python-SDK) 82 | * GO [Github](https://github.com/splitio/go-client) [Docs](https://help.split.io/hc/en-us/articles/360020093652-Go-SDK) 83 | * Android [Github](https://github.com/splitio/android-client) [Docs](https://help.split.io/hc/en-us/articles/360020343291-Android-SDK) 84 | * iOS [Github](https://github.com/splitio/ios-client) [Docs](https://help.split.io/hc/en-us/articles/360020401491-iOS-SDK) 85 | 86 | For a comprehensive list of open source projects visit our [Github page](https://github.com/splitio?utf8=%E2%9C%93&query=%20only%3Apublic%20). 87 | 88 | **Learn more about Split:** 89 | 90 | Visit [split.io/product](https://www.split.io/product) for an overview of Split, or visit our documentation at [help.split.io](http://help.split.io) for more detailed information. 91 | 92 | -------------------------------------------------------------------------------- /SplitOpenFeatureProvider.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 25.0.1704.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SplitOpenFeatureProvider", "SplitOpenFeatureProvider\SplitOpenFeatureProvider.csproj", "{09AC3885-FBF1-469A-A900-C14CE57D9DE1}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "OpenFeatureSample", "OpenFeatureSample", "{E97791C0-848A-4AA7-8E61-391BDB4FA11A}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeatureSample", "..\..\work\openfeature\OpenFeatureSample\OpenFeatureSample.csproj", "{9453938F-17D5-45D4-B2EE-302F707C2449}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProviderTests", "ProviderTests\ProviderTests.csproj", "{29BD56B4-6F46-4110-BEBF-28134DDD3620}" 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{9134287D-F99B-48B5-AB7F-F4E372BEAF67}" 15 | ProjectSection(SolutionItems) = preProject 16 | LICENSE = LICENSE 17 | README.md = README.md 18 | CONTRIBUTORS-GUIDE.md = CONTRIBUTORS-GUIDE.md 19 | SplitOpenFeatureProvider.sln = SplitOpenFeatureProvider.sln 20 | CHANGES.txt = CHANGES.txt 21 | EndProjectSection 22 | EndProject 23 | Global 24 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 25 | Debug|Any CPU = Debug|Any CPU 26 | Release|Any CPU = Release|Any CPU 27 | EndGlobalSection 28 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 29 | {09AC3885-FBF1-469A-A900-C14CE57D9DE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {09AC3885-FBF1-469A-A900-C14CE57D9DE1}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {09AC3885-FBF1-469A-A900-C14CE57D9DE1}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {09AC3885-FBF1-469A-A900-C14CE57D9DE1}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {9453938F-17D5-45D4-B2EE-302F707C2449}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {9453938F-17D5-45D4-B2EE-302F707C2449}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {9453938F-17D5-45D4-B2EE-302F707C2449}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {9453938F-17D5-45D4-B2EE-302F707C2449}.Release|Any CPU.Build.0 = Release|Any CPU 37 | {29BD56B4-6F46-4110-BEBF-28134DDD3620}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {29BD56B4-6F46-4110-BEBF-28134DDD3620}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {29BD56B4-6F46-4110-BEBF-28134DDD3620}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {29BD56B4-6F46-4110-BEBF-28134DDD3620}.Release|Any CPU.Build.0 = Release|Any CPU 41 | EndGlobalSection 42 | GlobalSection(SolutionProperties) = preSolution 43 | HideSolutionNode = FALSE 44 | EndGlobalSection 45 | GlobalSection(ExtensibilityGlobals) = postSolution 46 | SolutionGuid = {73FCC280-97C9-4130-8B80-50D9E9C3604F} 47 | EndGlobalSection 48 | GlobalSection(NestedProjects) = preSolution 49 | {9453938F-17D5-45D4-B2EE-302F707C2449} = {E97791C0-848A-4AA7-8E61-391BDB4FA11A} 50 | EndGlobalSection 51 | EndGlobal 52 | -------------------------------------------------------------------------------- /SplitOpenFeatureProvider/Provider.cs: -------------------------------------------------------------------------------- 1 | using OpenFeature; 2 | using OpenFeature.Model; 3 | using OpenFeature.Constant; 4 | using OpenFeature.Error; 5 | using Splitio.Services.Client.Interfaces; 6 | using Newtonsoft.Json.Linq; 7 | 8 | namespace Splitio.OpenFeature 9 | { 10 | public class Provider : FeatureProvider 11 | { 12 | private readonly Metadata _metadata = new("Split Client"); 13 | private readonly ISplitClient _client; 14 | 15 | public Provider(ISplitClient client) 16 | { 17 | _client = client; 18 | } 19 | 20 | public override Metadata GetMetadata() => _metadata; 21 | 22 | public override Task> ResolveBooleanValue(string flagKey, bool defaultValue, 23 | EvaluationContext context) 24 | { 25 | var key = GetTargetingKey(context); 26 | var originalResult = _client.GetTreatment(key, flagKey, TransformContext(context)); 27 | if (originalResult == "control") 28 | { 29 | return Task.FromResult(new ResolutionDetails(flagKey, defaultValue, variant: originalResult, errorType: ErrorType.FlagNotFound)); 30 | } 31 | var evaluationResult = ParseBoolean(originalResult); 32 | return Task.FromResult(new ResolutionDetails(flagKey, evaluationResult, errorType: ErrorType.None, variant: originalResult, reason: Reason.TargetingMatch)); 33 | } 34 | 35 | public override Task> ResolveStringValue(string flagKey, string defaultValue, 36 | EvaluationContext context) 37 | { 38 | var key = GetTargetingKey(context); 39 | var evaluationResult = _client.GetTreatment(key, flagKey, TransformContext(context)); 40 | if (evaluationResult == "control") 41 | { 42 | return Task.FromResult(new ResolutionDetails(flagKey, defaultValue, variant: evaluationResult, errorType: ErrorType.FlagNotFound)); 43 | } 44 | return Task.FromResult(new ResolutionDetails(flagKey, evaluationResult ?? defaultValue, variant: evaluationResult, reason: Reason.TargetingMatch, errorType: ErrorType.None)); 45 | } 46 | 47 | public override Task> ResolveIntegerValue(string flagKey, int defaultValue, 48 | EvaluationContext context) 49 | { 50 | var key = GetTargetingKey(context); 51 | var originalResult = _client.GetTreatment(key, flagKey, TransformContext(context)); 52 | if (originalResult == "control") 53 | { 54 | return Task.FromResult(new ResolutionDetails(flagKey, defaultValue, variant: originalResult, errorType: ErrorType.FlagNotFound)); 55 | } 56 | try { 57 | var evaluationResult = int.Parse(originalResult); 58 | return Task.FromResult(new ResolutionDetails(flagKey, evaluationResult, variant: originalResult, reason: Reason.TargetingMatch, errorType: ErrorType.None)); 59 | } 60 | catch (FormatException) 61 | { 62 | throw new FeatureProviderException(ErrorType.ParseError, $"{originalResult} is not an int"); 63 | }; 64 | } 65 | 66 | public override Task> ResolveDoubleValue(string flagKey, double defaultValue, 67 | EvaluationContext context) 68 | { 69 | var key = GetTargetingKey(context); 70 | var originalResult = _client.GetTreatment(key, flagKey, TransformContext(context)); 71 | if (originalResult == "control") 72 | { 73 | return Task.FromResult(new ResolutionDetails(flagKey, defaultValue, variant: originalResult, errorType: ErrorType.FlagNotFound)); 74 | } 75 | try 76 | { 77 | var evaluationResult = double.Parse(originalResult); 78 | return Task.FromResult(new ResolutionDetails(flagKey, evaluationResult, variant: originalResult, reason: Reason.TargetingMatch, errorType: ErrorType.None)); 79 | } 80 | catch (FormatException) 81 | { 82 | throw new FeatureProviderException(ErrorType.ParseError, $"{originalResult} is not a double"); 83 | } 84 | } 85 | 86 | public override Task> ResolveStructureValue(string flagKey, Value defaultValue, EvaluationContext context) 87 | { 88 | var key = GetTargetingKey(context); 89 | var originalResult = _client.GetTreatmentWithConfig(key, flagKey, TransformContext(context)); 90 | if (originalResult.Treatment == "control") 91 | { 92 | return Task.FromResult(new ResolutionDetails(flagKey, defaultValue, variant: originalResult.Treatment, errorType: ErrorType.FlagNotFound)); 93 | } 94 | try { 95 | var jsonString = originalResult.Config; 96 | var dict = JObject.Parse(jsonString).ToObject>(); 97 | if (dict == null) 98 | { 99 | throw new FeatureProviderException(ErrorType.ParseError, $"{originalResult.Config} is not an object"); 100 | } 101 | var dict2 = dict.ToDictionary(x => x.Key, x => new Value(x.Value)); 102 | var dictValue = new Value(new Structure(dict2)); 103 | return Task.FromResult(new ResolutionDetails(flagKey, dictValue, variant: originalResult.Treatment, reason: Reason.TargetingMatch, errorType: ErrorType.None)); 104 | } 105 | catch (Exception ex) 106 | { 107 | Console.WriteLine($"Exception parsing JSON: {ex}"); 108 | Console.WriteLine($"Attempted to parse: {originalResult.Config}"); 109 | throw new FeatureProviderException(ErrorType.ParseError, $"{originalResult.Config} is not an object"); 110 | } 111 | } 112 | 113 | private static string GetTargetingKey(EvaluationContext context) 114 | { 115 | Value key; 116 | if (!context.TryGetValue("targetingKey", out key)) 117 | { 118 | Console.WriteLine("Split provider: targeting key missing!"); 119 | throw new FeatureProviderException(ErrorType.TargetingKeyMissing, "Split provider requires a userkey"); 120 | } 121 | return key.AsString; 122 | } 123 | 124 | private static Dictionary TransformContext(EvaluationContext context) 125 | { 126 | return context == null 127 | ? new Dictionary() 128 | : context.AsDictionary().ToDictionary(x => x.Key, x => x.Value.AsObject); 129 | } 130 | 131 | private static bool ParseBoolean(string boolStr) 132 | { 133 | return boolStr.ToLower() switch 134 | { 135 | "on" or "true" => true, 136 | "off" or "false" => false, 137 | _ => throw new FeatureProviderException(ErrorType.ParseError, $"{boolStr} is not a boolean"), 138 | }; 139 | } 140 | } 141 | } -------------------------------------------------------------------------------- /SplitOpenFeatureProvider/SplitOpenFeatureProvider.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | --------------------------------------------------------------------------------