From 8de6a278160c2d53d0a9d137570b7aac7ff2050c Mon Sep 17 00:00:00 2001
From: Md Junaed Hossain <169046794+junaed-optimizely@users.noreply.github.com>
Date: Fri, 19 Sep 2025 21:56:35 +0600
Subject: [PATCH 1/3] [FSSDK-11138] update project config
---
.../OptimizelySDK.Net35.csproj | 3 +
.../OptimizelySDK.Net40.csproj | 3 +
.../OptimizelySDK.NetStandard16.csproj | 1 +
.../OptimizelySDK.NetStandard20.csproj | 3 +
OptimizelySDK.Tests/ProjectConfigTest.cs | 35 +++++++++++
OptimizelySDK/Config/DatafileProjectConfig.cs | 28 +++++++++
OptimizelySDK/Entity/CmabConfig.cs | 63 +++++++++++++++++++
OptimizelySDK/Entity/Experiment.cs | 7 +++
OptimizelySDK/OptimizelySDK.csproj | 1 +
OptimizelySDK/ProjectConfig.cs | 12 ++++
OptimizelySDK/Utils/schema.json | 16 ++++-
11 files changed, 171 insertions(+), 1 deletion(-)
create mode 100644 OptimizelySDK/Entity/CmabConfig.cs
diff --git a/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj b/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj
index 303a742e..1cf0a929 100644
--- a/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj
+++ b/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj
@@ -88,6 +88,9 @@
Entity\Experiment.cs
+
+ Entity\CmabConfig.cs
+
Entity\Holdout.cs
diff --git a/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj b/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj
index 3e0a9ea5..835895a2 100644
--- a/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj
+++ b/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj
@@ -90,6 +90,9 @@
Entity\Experiment.cs
+
+ Entity\CmabConfig.cs
+
Entity\Holdout.cs
diff --git a/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj b/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj
index ab55bbac..6949973c 100644
--- a/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj
+++ b/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj
@@ -26,6 +26,7 @@
+
diff --git a/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj b/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj
index 12f9cb55..0ec76a6c 100644
--- a/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj
+++ b/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj
@@ -178,6 +178,9 @@
Entity\Experiment.cs
+
+ Entity\CmabConfig.cs
+
Entity\Holdout.cs
diff --git a/OptimizelySDK.Tests/ProjectConfigTest.cs b/OptimizelySDK.Tests/ProjectConfigTest.cs
index 15e95926..52373b2d 100644
--- a/OptimizelySDK.Tests/ProjectConfigTest.cs
+++ b/OptimizelySDK.Tests/ProjectConfigTest.cs
@@ -1455,5 +1455,40 @@ public void TestMissingHoldoutsField_BackwardCompatibility()
}
#endregion
+
+ [Test]
+ public void TestCmabFieldPopulation()
+ {
+
+ var datafileJson = JObject.Parse(TestData.Datafile);
+ var experiments = (JArray)datafileJson["experiments"];
+
+ if (experiments.Count > 0)
+ {
+ var firstExperiment = (JObject)experiments[0];
+
+ firstExperiment["cmab"] = new JObject
+ {
+ ["attributeIds"] = new JArray { "7723280020", "7723348204" },
+ ["trafficAllocation"] = 4000
+ };
+
+ firstExperiment["trafficAllocation"] = new JArray();
+ }
+
+ var modifiedDatafile = datafileJson.ToString();
+ var projectConfig = DatafileProjectConfig.Create(modifiedDatafile, LoggerMock.Object, ErrorHandlerMock.Object);
+ var experimentWithCmab = projectConfig.GetExperimentFromKey("test_experiment");
+
+ Assert.IsNotNull(experimentWithCmab.Cmab);
+ Assert.AreEqual(2, experimentWithCmab.Cmab.AttributeIds.Count);
+ Assert.Contains("7723280020", experimentWithCmab.Cmab.AttributeIds);
+ Assert.Contains("7723348204", experimentWithCmab.Cmab.AttributeIds);
+ Assert.AreEqual(4000, experimentWithCmab.Cmab.TrafficAllocation);
+
+ var experimentWithoutCmab = projectConfig.GetExperimentFromKey("paused_experiment");
+
+ Assert.IsNull(experimentWithoutCmab.Cmab);
+ }
}
}
diff --git a/OptimizelySDK/Config/DatafileProjectConfig.cs b/OptimizelySDK/Config/DatafileProjectConfig.cs
index e701bc4e..52593e78 100644
--- a/OptimizelySDK/Config/DatafileProjectConfig.cs
+++ b/OptimizelySDK/Config/DatafileProjectConfig.cs
@@ -195,6 +195,13 @@ private Dictionary> _VariationIdMap
public Dictionary AttributeKeyMap => _AttributeKeyMap;
+ ///
+ /// Associative array of attribute ID to Attribute(s) in the datafile
+ ///
+ private Dictionary _AttributeIdMap;
+
+ public Dictionary AttributeIdMap => _AttributeIdMap;
+
///
/// Associative array of audience ID to Audience(s) in the datafile
///
@@ -332,6 +339,8 @@ private void Initialize()
true);
_AttributeKeyMap = ConfigParser.GenerateMap(Attributes,
a => a.Key, true);
+ _AttributeIdMap = ConfigParser.GenerateMap(Attributes,
+ a => a.Id, true);
_AudienceIdMap = ConfigParser.GenerateMap(Audiences,
a => a.Id.ToString(), true);
_FeatureKeyMap = ConfigParser.GenerateMap(FeatureFlags,
@@ -653,6 +662,25 @@ public Attribute GetAttribute(string attributeKey)
return new Attribute();
}
+ ///
+ /// Get the Attribute from the ID
+ ///
+ /// ID of the Attribute
+ /// Attribute Entity corresponding to the ID or a dummy entity if ID is invalid
+ public Attribute GetAttributeById(string attributeId)
+ {
+ if (_AttributeIdMap.ContainsKey(attributeId))
+ {
+ return _AttributeIdMap[attributeId];
+ }
+
+ var message = $@"Attribute ID ""{attributeId}"" is not in datafile.";
+ Logger.Log(LogLevel.ERROR, message);
+ ErrorHandler.HandleError(
+ new InvalidAttributeException("Provided attribute is not in datafile."));
+ return new Attribute();
+ }
+
///
/// Get the Variation from the keys
///
diff --git a/OptimizelySDK/Entity/CmabConfig.cs b/OptimizelySDK/Entity/CmabConfig.cs
new file mode 100644
index 00000000..5615ed6a
--- /dev/null
+++ b/OptimizelySDK/Entity/CmabConfig.cs
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2025, Optimizely
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using System.Collections.Generic;
+using Newtonsoft.Json;
+
+namespace OptimizelySDK.Entity
+{
+ ///
+ /// Class representing CMAB (Contextual Multi-Armed Bandit) configuration for experiments.
+ ///
+ public class CmabConfig
+ {
+ ///
+ /// List of attribute IDs that are relevant for CMAB decision making.
+ /// These attributes will be used to filter user attributes when making CMAB requests.
+ ///
+ [JsonProperty("attributeIds")]
+ public List AttributeIds { get; set; }
+
+ ///
+ /// Traffic allocation value for CMAB experiments.
+ /// Determines what portion of traffic should be allocated to CMAB decision making.
+ ///
+ [JsonProperty("trafficAllocation")]
+ public int? TrafficAllocation { get; set; }
+
+ ///
+ /// Initializes a new instance of the CmabConfig class with specified values.
+ ///
+ /// List of attribute IDs for CMAB
+ /// Traffic allocation value
+ public CmabConfig(List attributeIds, int? trafficAllocation = null)
+ {
+ AttributeIds = attributeIds ?? new List();
+ TrafficAllocation = trafficAllocation;
+ }
+
+ ///
+ /// Returns a string representation of the CMAB configuration.
+ ///
+ /// String representation
+ public override string ToString()
+ {
+ var attributeList = AttributeIds ?? new List();
+ return string.Format("CmabDict{{AttributeIds=[{0}], TrafficAllocation={1}}}",
+ string.Join(", ", attributeList.ToArray()), TrafficAllocation);
+ }
+ }
+}
diff --git a/OptimizelySDK/Entity/Experiment.cs b/OptimizelySDK/Entity/Experiment.cs
index 8a7b4036..0ca5993a 100644
--- a/OptimizelySDK/Entity/Experiment.cs
+++ b/OptimizelySDK/Entity/Experiment.cs
@@ -15,6 +15,7 @@
*/
using System.Collections.Generic;
+using Newtonsoft.Json;
namespace OptimizelySDK.Entity
{
@@ -48,6 +49,12 @@ public class Experiment : ExperimentCore
public bool IsInMutexGroup =>
!string.IsNullOrEmpty(GroupPolicy) && GroupPolicy == MUTEX_GROUP_POLICY;
+ ///
+ /// CMAB (Contextual Multi-Armed Bandit) configuration for the experiment.
+ ///
+ [JsonProperty("cmab")]
+ public CmabConfig Cmab { get; set; }
+
///
/// Determin if user is forced variation of experiment
///
diff --git a/OptimizelySDK/OptimizelySDK.csproj b/OptimizelySDK/OptimizelySDK.csproj
index 14201b4e..946b4a0d 100644
--- a/OptimizelySDK/OptimizelySDK.csproj
+++ b/OptimizelySDK/OptimizelySDK.csproj
@@ -84,6 +84,7 @@
+
diff --git a/OptimizelySDK/ProjectConfig.cs b/OptimizelySDK/ProjectConfig.cs
index 992c900b..28f63d24 100644
--- a/OptimizelySDK/ProjectConfig.cs
+++ b/OptimizelySDK/ProjectConfig.cs
@@ -113,6 +113,11 @@ public interface ProjectConfig
///
Dictionary AttributeKeyMap { get; }
+ ///
+ /// Associative array of attribute ID to Attribute(s) in the datafile
+ ///
+ Dictionary AttributeIdMap { get; }
+
///
/// Associative array of audience ID to Audience(s) in the datafile
///
@@ -234,6 +239,13 @@ public interface ProjectConfig
/// Attribute Entity corresponding to the key or a dummy entity if key is invalid
Attribute GetAttribute(string attributeKey);
+ ///
+ /// Get the Attribute from the ID
+ ///
+ /// ID of the Attribute
+ /// Attribute Entity corresponding to the ID or a dummy entity if ID is invalid
+ Attribute GetAttributeById(string attributeId);
+
///
/// Get the Variation from the keys
///
diff --git a/OptimizelySDK/Utils/schema.json b/OptimizelySDK/Utils/schema.json
index c434bbac..dd3fedef 100644
--- a/OptimizelySDK/Utils/schema.json
+++ b/OptimizelySDK/Utils/schema.json
@@ -182,6 +182,20 @@
},
"forcedVariations": {
"type": "object"
+ },
+ "cmab": {
+ "type": "object",
+ "properties": {
+ "attributeIds": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "trafficAllocation": {
+ "type": "integer"
+ }
+ }
}
},
"required": [
@@ -279,4 +293,4 @@
"version",
"revision"
]
-}
\ No newline at end of file
+}
From 54e13fabe331b92e143e54ab4e930d443745ad06 Mon Sep 17 00:00:00 2001
From: Md Junaed Hossain <169046794+junaed-optimizely@users.noreply.github.com>
Date: Sat, 20 Sep 2025 00:21:38 +0600
Subject: [PATCH 2/3] [FSSDK-11138] config parser update
---
OptimizelySDK/Utils/ConfigParser.cs | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/OptimizelySDK/Utils/ConfigParser.cs b/OptimizelySDK/Utils/ConfigParser.cs
index 318ff1b6..c6b86916 100644
--- a/OptimizelySDK/Utils/ConfigParser.cs
+++ b/OptimizelySDK/Utils/ConfigParser.cs
@@ -33,7 +33,13 @@ public static Dictionary GenerateMap(IEnumerable entities,
Func getKey, bool clone
)
{
- return entities.ToDictionary(e => getKey(e), e => clone ? (T)e.Clone() : e);
+ var dictionary = new Dictionary();
+ foreach (var entity in entities)
+ {
+ var key = getKey(entity);
+ dictionary[key] = clone ? (T)entity.Clone() : entity;
+ }
+ return dictionary;
}
}
}
From 63cd446ac54782bf20c110ce09fb90032eac17f4 Mon Sep 17 00:00:00 2001
From: Md Junaed Hossain <169046794+junaed-optimizely@users.noreply.github.com>
Date: Mon, 22 Sep 2025 20:42:37 +0600
Subject: [PATCH 3/3] [FSSDK-11138] review update
---
OptimizelySDK.Net35/OptimizelySDK.Net35.csproj | 4 ++--
OptimizelySDK.Net40/OptimizelySDK.Net40.csproj | 4 ++--
.../OptimizelySDK.NetStandard16.csproj | 2 +-
.../OptimizelySDK.NetStandard20.csproj | 4 ++--
OptimizelySDK/Entity/{CmabConfig.cs => Cmab.cs} | 8 ++++----
OptimizelySDK/Entity/Experiment.cs | 2 +-
OptimizelySDK/OptimizelySDK.csproj | 2 +-
7 files changed, 13 insertions(+), 13 deletions(-)
rename OptimizelySDK/Entity/{CmabConfig.cs => Cmab.cs} (88%)
diff --git a/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj b/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj
index 1cf0a929..e441df53 100644
--- a/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj
+++ b/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj
@@ -88,8 +88,8 @@
Entity\Experiment.cs
-
- Entity\CmabConfig.cs
+
+ Entity\Cmab.cs
Entity\Holdout.cs
diff --git a/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj b/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj
index 835895a2..c1150280 100644
--- a/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj
+++ b/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj
@@ -90,8 +90,8 @@
Entity\Experiment.cs
-
- Entity\CmabConfig.cs
+
+ Entity\Cmab.cs
Entity\Holdout.cs
diff --git a/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj b/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj
index 6949973c..1490ba14 100644
--- a/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj
+++ b/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj
@@ -26,7 +26,7 @@
-
+
diff --git a/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj b/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj
index 0ec76a6c..418f606d 100644
--- a/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj
+++ b/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj
@@ -178,8 +178,8 @@
Entity\Experiment.cs
-
- Entity\CmabConfig.cs
+
+ Entity\Cmab.cs
Entity\Holdout.cs
diff --git a/OptimizelySDK/Entity/CmabConfig.cs b/OptimizelySDK/Entity/Cmab.cs
similarity index 88%
rename from OptimizelySDK/Entity/CmabConfig.cs
rename to OptimizelySDK/Entity/Cmab.cs
index 5615ed6a..f8caec87 100644
--- a/OptimizelySDK/Entity/CmabConfig.cs
+++ b/OptimizelySDK/Entity/Cmab.cs
@@ -22,7 +22,7 @@ namespace OptimizelySDK.Entity
///
/// Class representing CMAB (Contextual Multi-Armed Bandit) configuration for experiments.
///
- public class CmabConfig
+ public class Cmab
{
///
/// List of attribute IDs that are relevant for CMAB decision making.
@@ -39,11 +39,11 @@ public class CmabConfig
public int? TrafficAllocation { get; set; }
///
- /// Initializes a new instance of the CmabConfig class with specified values.
+ /// Initializes a new instance of the Cmab class with specified values.
///
/// List of attribute IDs for CMAB
/// Traffic allocation value
- public CmabConfig(List attributeIds, int? trafficAllocation = null)
+ public Cmab(List attributeIds, int? trafficAllocation = null)
{
AttributeIds = attributeIds ?? new List();
TrafficAllocation = trafficAllocation;
@@ -56,7 +56,7 @@ public CmabConfig(List attributeIds, int? trafficAllocation = null)
public override string ToString()
{
var attributeList = AttributeIds ?? new List();
- return string.Format("CmabDict{{AttributeIds=[{0}], TrafficAllocation={1}}}",
+ return string.Format("Cmab{{AttributeIds=[{0}], TrafficAllocation={1}}}",
string.Join(", ", attributeList.ToArray()), TrafficAllocation);
}
}
diff --git a/OptimizelySDK/Entity/Experiment.cs b/OptimizelySDK/Entity/Experiment.cs
index 0ca5993a..52e99015 100644
--- a/OptimizelySDK/Entity/Experiment.cs
+++ b/OptimizelySDK/Entity/Experiment.cs
@@ -53,7 +53,7 @@ public class Experiment : ExperimentCore
/// CMAB (Contextual Multi-Armed Bandit) configuration for the experiment.
///
[JsonProperty("cmab")]
- public CmabConfig Cmab { get; set; }
+ public Cmab Cmab { get; set; }
///
/// Determin if user is forced variation of experiment
diff --git a/OptimizelySDK/OptimizelySDK.csproj b/OptimizelySDK/OptimizelySDK.csproj
index 946b4a0d..b4537b92 100644
--- a/OptimizelySDK/OptimizelySDK.csproj
+++ b/OptimizelySDK/OptimizelySDK.csproj
@@ -84,7 +84,7 @@
-
+