diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 0000000..0b7f9e5
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,20 @@
+name: CI
+on: [push]
+jobs:
+ build:
+ runs-on: windows-2019
+ steps:
+ - uses: actions/checkout@v3
+
+ - uses: actions/setup-dotnet@v3
+ with:
+ dotnet-version: '6.0.x'
+ dotnet-quality: 'preview'
+ env:
+ NUGET_AUTH_TOKEN: ${{secrets.NUGET_AUTH_TOKEN}}
+
+ - name: Run Build
+ run: dotnet build src/ABSmartly.Sdk
+
+ - name: Run Tests
+ run: dotnet test tests/ABSmartly.Sdk.Tests
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
new file mode 100644
index 0000000..5c99bf4
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,30 @@
+name: Publish Nuget
+on:
+ push:
+ branches:
+ - "main"
+ tags:
+ - v*
+jobs:
+ build:
+ runs-on: windows-2019
+
+ steps:
+ - uses: actions/checkout@v3
+
+ - uses: actions/setup-dotnet@v3
+ with:
+ dotnet-version: '5.0.x'
+ dotnet-quality: 'preview'
+ env:
+ NUGET_AUTH_TOKEN: ${{secrets.NUGET_AUTH_TOKEN}}
+
+ - name: Run Build
+ run: dotnet build src/ABSmartly.Sdk
+ env:
+ NUGET_AUTH_TOKEN: ${{secrets.NUGET_AUTH_TOKEN}}
+
+ - name: Run Release
+ run: dotnet nuget push src/ABSmartly.Sdk/bin/Debug/ --api-key $NUGET_AUTH_TOKEN --source https://api.nuget.org/v3/index.json
+ env:
+ NUGET_AUTH_TOKEN: ${{secrets.NUGET_AUTH_TOKEN}}
\ No newline at end of file
diff --git a/src/ABSmartly.Sdk/ABSmartly.Sdk.csproj b/src/ABSmartly.Sdk/ABSmartly.Sdk.csproj
index d346e40..990b45b 100644
--- a/src/ABSmartly.Sdk/ABSmartly.Sdk.csproj
+++ b/src/ABSmartly.Sdk/ABSmartly.Sdk.csproj
@@ -6,7 +6,7 @@
true
ABSmartly.Sdk
true
- 1.1.1
+ 1.2.1
A/B Smartly DotNet SDK
A/B Smartly
The A/B Smartly DotNet SDK is a client SDK for A/B Smartly service
@@ -19,11 +19,11 @@
net5.0;netstandard2.0
1.0.0.0
1.1.1.77
+ true
-
diff --git a/src/ABSmartly.Sdk/Context.cs b/src/ABSmartly.Sdk/Context.cs
index ad17eaa..ade647c 100644
--- a/src/ABSmartly.Sdk/Context.cs
+++ b/src/ABSmartly.Sdk/Context.cs
@@ -55,6 +55,8 @@ public class Context : IContext, IDisposable, IAsyncDisposable
private bool _failed;
private Dictionary _index;
+ private Dictionary> _contextCustomFields;
+
private DictionaryLockableAdapter _indexVariables;
private volatile int _pendingCount;
@@ -460,6 +462,84 @@ private ExperimentVariables GetExperiment(string experimentName)
_dataLock.ExitReadLock();
}
}
+
+ public List GetCustomFieldKeys()
+ {
+ try
+ {
+ _dataLock.EnterReadLock();
+
+ var keys = new List();
+
+ foreach (var experiment in _data.Experiments)
+ {
+ var customFieldValues = experiment.CustomFieldValues;
+ if (customFieldValues != null)
+ {
+ foreach (var customFieldValue in customFieldValues)
+ {
+ keys.Add(customFieldValue.Name);
+ }
+ }
+ }
+
+ return keys.OrderBy(q => q).Distinct().ToList();
+ }
+ finally
+ {
+ _dataLock.ExitReadLock();
+ }
+ }
+
+ public Object GetCustomFieldValue(String environmentName, String key)
+ {
+ try
+ {
+ _dataLock.EnterReadLock();
+
+ _contextCustomFields.TryGetValue(environmentName, out var customFieldValues);
+
+ if (customFieldValues != null)
+ {
+ customFieldValues.TryGetValue(key, out var field);
+ if (field != null)
+ {
+ return field.Value;
+ }
+ }
+
+ return null;
+ }
+ finally
+ {
+ _dataLock.ExitReadLock();
+ }
+ }
+
+ public Object GetCustomFieldType(String environmentName, String key)
+ {
+ try
+ {
+ _dataLock.EnterReadLock();
+
+ _contextCustomFields.TryGetValue(environmentName, out var customFieldValues);
+
+ if (customFieldValues != null)
+ {
+ customFieldValues.TryGetValue(key, out var field);
+ if (field != null)
+ {
+ return field.Type;
+ }
+ }
+
+ return null;
+ }
+ finally
+ {
+ _dataLock.ExitReadLock();
+ }
+ }
private ExperimentVariables GetVariableExperiment(string key)
{
@@ -854,6 +934,7 @@ private void SetData(ContextData data)
{
var index = new Dictionary();
var indexVariables = new Dictionary();
+ var contextCustomFields = new Dictionary>();
foreach (var experiment in data.Experiments)
{
@@ -878,6 +959,43 @@ private void SetData(ContextData data)
}
index[experiment.Name] = experimentVariables;
+
+ if (experiment.CustomFieldValues == null) continue;
+
+ var experimentCustomFields = new Dictionary();
+ foreach (var customFieldValue in experiment.CustomFieldValues)
+ {
+ var value = new ContextCustomFieldValue
+ {
+ Type = customFieldValue.Type
+ };
+
+ if (customFieldValue.Value != null)
+ {
+ var customValue = customFieldValue.Value;
+
+ if (customFieldValue.Type.StartsWith("json"))
+ {
+ value.Value = _variableParser.Parse(this, experiment.Name, customFieldValue.Name, customValue);
+ }
+ else if(customFieldValue.Type.StartsWith("boolean"))
+ {
+ value.Value = Convert.ToBoolean(customValue);
+ }
+ else if(customFieldValue.Type.StartsWith("number"))
+ {
+ value.Value = Convert.ToInt64(customValue);
+ }
+ else
+ {
+ value.Value = customValue;
+ }
+ }
+
+ experimentCustomFields[customFieldValue.Name] = value;
+ }
+
+ contextCustomFields[experiment.Name] = experimentCustomFields;
}
try
@@ -885,6 +1003,7 @@ private void SetData(ContextData data)
_dataLock.EnterWriteLock();
_index = index;
+ _contextCustomFields = contextCustomFields;
_indexVariables =
new DictionaryLockableAdapter(new LockableCollectionSlimLock(_dataLock),
indexVariables);
@@ -998,6 +1117,13 @@ public class ExperimentVariables
public Experiment Data { get; set; }
public List> Variables { get; set; }
}
+
+ public class ContextCustomFieldValue
+ {
+ public String Name { get; set; }
+ public String Type { get; set; }
+ public Object Value { get; set; }
+ }
public class Assignment
{
diff --git a/src/ABSmartly.Sdk/Models/CustomFieldValue.cs b/src/ABSmartly.Sdk/Models/CustomFieldValue.cs
new file mode 100644
index 0000000..e13208d
--- /dev/null
+++ b/src/ABSmartly.Sdk/Models/CustomFieldValue.cs
@@ -0,0 +1,46 @@
+using System.Diagnostics;
+
+namespace ABSmartly.Models;
+
+[DebuggerDisplay("{DebugView},nq")]
+public class CustomFieldValue
+{
+ public string Name { get; set; }
+ public string Type { get; set; }
+ public string Value { get; set; }
+
+ private string DebugView => $"ExperimentVariant{{name={Name}, type={Type}, value={Value}}}";
+
+ public override string ToString()
+ {
+ return DebugView;
+ }
+
+
+ #region Equality members
+
+ protected bool Equals(CustomFieldValue other)
+ {
+ return Name == other.Name &&
+ Type == other.Type&&
+ Value == other.Value;
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (ReferenceEquals(null, obj)) return false;
+ if (ReferenceEquals(this, obj)) return true;
+ if (obj.GetType() != GetType()) return false;
+ return Equals((CustomFieldValue)obj);
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ return ((Name?.GetHashCode() ?? 0) * 397) ^ (Type?.GetHashCode() ?? 0) ^ (Value?.GetHashCode() ?? 0);
+ }
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/src/ABSmartly.Sdk/Models/Experiment.cs b/src/ABSmartly.Sdk/Models/Experiment.cs
index 54f9a29..bf0fc26 100644
--- a/src/ABSmartly.Sdk/Models/Experiment.cs
+++ b/src/ABSmartly.Sdk/Models/Experiment.cs
@@ -20,6 +20,7 @@ public class Experiment
public int FullOnVariant { get; set; }
public ExperimentApplication[] Applications { get; set; }
public ExperimentVariant[] Variants { get; set; }
+ public CustomFieldValue[] CustomFieldValues { get; set; }
public bool AudienceStrict { get; set; }
public string Audience { get; set; }
diff --git a/tests/ABSmartly.Sdk.Tests/ContextTests.cs b/tests/ABSmartly.Sdk.Tests/ContextTests.cs
index b9af4ca..71487f7 100644
--- a/tests/ABSmartly.Sdk.Tests/ContextTests.cs
+++ b/tests/ABSmartly.Sdk.Tests/ContextTests.cs
@@ -770,6 +770,51 @@ public void TestGetVariableKeys()
context.GetVariableKeys().Should().BeEquivalentTo(_variableExperiments);
}
+
+ [Test]
+ public void TestGetCustomFieldKeys()
+ {
+ var context = CreateContext(_data);
+
+ context.GetCustomFieldKeys().Should().BeEquivalentTo(new List { "country", "languages", "overrides" });
+ }
+
+ [Test]
+ public void TestGetCustomFieldValues()
+ {
+ var context = CreateContext(_data);
+
+ context.GetCustomFieldValue("not_found", "not_found").Should().BeNull();
+ context.GetCustomFieldValue("exp_test_ab", "not_found").Should().BeNull();
+ context.GetCustomFieldValue("exp_test_ab", "country").Should().BeEquivalentTo("US,PT,ES,DE,FR");
+ context.GetCustomFieldType("exp_test_ab", "country").Should().BeEquivalentTo("string");
+
+ context.GetCustomFieldValue("exp_test_ab", "overrides").Should().BeEquivalentTo(new Dictionary
+ {
+ { "123", 1 },
+ { "456", 0 }
+ }
+ );
+ context.GetCustomFieldType("exp_test_ab", "overrides").Should().BeEquivalentTo("json");
+
+ context.GetCustomFieldValue("exp_test_ab", "languages").Should().BeNull();
+ context.GetCustomFieldValue("exp_test_ab", "languages").Should().BeNull();
+
+ context.GetCustomFieldValue("exp_test_abc", "overrides").Should().BeNull();
+ context.GetCustomFieldValue("exp_test_abc", "overrides").Should().BeNull();
+
+ context.GetCustomFieldValue("exp_test_abc", "languages").Should().BeEquivalentTo("en-US,en-GB,pt-PT,pt-BR,es-ES,es-MX");
+ context.GetCustomFieldType("exp_test_abc", "languages").Should().BeEquivalentTo("string");
+
+ context.GetCustomFieldValue("exp_test_no_custom_fields", "country").Should().BeNull();
+ context.GetCustomFieldValue("exp_test_no_custom_fields", "country").Should().BeNull();
+
+ context.GetCustomFieldValue("exp_test_no_custom_fields", "overrides").Should().BeNull();
+ context.GetCustomFieldValue("exp_test_no_custom_fields", "overrides").Should().BeNull();
+
+ context.GetCustomFieldValue("exp_test_no_custom_fields", "languages").Should().BeNull();
+ context.GetCustomFieldValue("exp_test_no_custom_fields", "languages").Should().BeNull();
+ }
[Test]
public void TestPeekTreatmentReturnsOverrideVariant()
diff --git a/tests/ABSmartly.Sdk.Tests/Resources/context.json b/tests/ABSmartly.Sdk.Tests/Resources/context.json
index c2f0066..c4340aa 100644
--- a/tests/ABSmartly.Sdk.Tests/Resources/context.json
+++ b/tests/ABSmartly.Sdk.Tests/Resources/context.json
@@ -33,7 +33,19 @@
"config":"{\"banner.border\":1,\"banner.size\":\"large\"}"
}
],
- "audience": null
+ "audience": null,
+ "customFieldValues": [
+ {
+ "name": "country",
+ "value": "US,PT,ES,DE,FR",
+ "type": "string"
+ },
+ {
+ "name": "overrides",
+ "value": "{\"123\":1,\"456\":0}",
+ "type": "json"
+ }
+ ]
},
{
"id":2,
@@ -73,7 +85,19 @@
"config":"{\"button.color\":\"red\"}"
}
],
- "audience": ""
+ "audience": "",
+ "customFieldValues": [
+ {
+ "name": "country",
+ "value": "US,PT,ES,DE,FR",
+ "type": "string"
+ },
+ {
+ "name": "languages",
+ "value": "en-US,en-GB,pt-PT,pt-BR,es-ES,es-MX",
+ "type": "string"
+ }
+ ]
},
{
"id":3,
@@ -113,7 +137,8 @@
"config":"{\"card.width\":\"75%\"}"
}
],
- "audience": "{}"
+ "audience": "{}",
+ "customFieldValues": null
},
{
"id":4,