From 01bad8fbdd339440a445fb0e867d7ae281aa15cc Mon Sep 17 00:00:00 2001
From: muhammadnoman <muhammadnoman@folio3.com>
Date: Thu, 5 Nov 2020 20:48:30 +0500
Subject: [PATCH 01/19] Added OptimizelyUserContext and basic functionalities
 and classes related to it

---
 .../OptimizelyDecisions/DecisionMessage.cs    |  32 +++++
 .../OptimizelyDecisions/DecisionReasons.cs    |  31 +++++
 .../DefaultDecisionReasons.cs                 |  59 ++++++++
 .../ErrorsDecisionReasons.cs                  |  42 ++++++
 .../OptimizelyDecideOption.cs                 |  27 ++++
 .../OptimizelyDecisions/OptimizelyDecision.cs |  66 +++++++++
 OptimizelySDK/OptimizelyUserContext.cs        | 130 ++++++++++++++++++
 7 files changed, 387 insertions(+)
 create mode 100644 OptimizelySDK/OptimizelyDecisions/DecisionMessage.cs
 create mode 100644 OptimizelySDK/OptimizelyDecisions/DecisionReasons.cs
 create mode 100644 OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs
 create mode 100644 OptimizelySDK/OptimizelyDecisions/ErrorsDecisionReasons.cs
 create mode 100644 OptimizelySDK/OptimizelyDecisions/OptimizelyDecideOption.cs
 create mode 100644 OptimizelySDK/OptimizelyDecisions/OptimizelyDecision.cs
 create mode 100644 OptimizelySDK/OptimizelyUserContext.cs

diff --git a/OptimizelySDK/OptimizelyDecisions/DecisionMessage.cs b/OptimizelySDK/OptimizelyDecisions/DecisionMessage.cs
new file mode 100644
index 00000000..ab2d1214
--- /dev/null
+++ b/OptimizelySDK/OptimizelyDecisions/DecisionMessage.cs
@@ -0,0 +1,32 @@
+/* 
+ * Copyright 2020, 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.
+ */
+
+namespace OptimizelySDK.OptimizelyDecisions
+{
+    public class DecisionMessage
+    {
+        public const string SDK_NOT_READY = "Optimizely SDK not configured properly yet.";
+        public const string FLAG_KEY_INVALID = "No flag was found for key \"%s\".";
+        public const string VARIABLE_VALUE_INVALID = "Variable value for key \"%s\" is invalid or wrong type.";
+
+        private string Format { get; set; }
+
+        public string Reason(params object[] args)
+        {
+            return string.Format(Format, args);
+        }
+    }
+}
diff --git a/OptimizelySDK/OptimizelyDecisions/DecisionReasons.cs b/OptimizelySDK/OptimizelyDecisions/DecisionReasons.cs
new file mode 100644
index 00000000..c1c0820b
--- /dev/null
+++ b/OptimizelySDK/OptimizelyDecisions/DecisionReasons.cs
@@ -0,0 +1,31 @@
+/* 
+ * Copyright 2020, 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;
+
+namespace OptimizelySDK.OptimizelyDecisions
+{
+    /// <summary>
+    /// Interface implemented by all condition classes for audience evaluation.
+    /// </summary>
+    public interface IDecisionReasons
+    {
+        void AddError(string format, params object[] args);
+        string AddInfo(string format, params object[] args);
+        List<string> ToReport();
+    }
+
+}
diff --git a/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs b/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs
new file mode 100644
index 00000000..d7c48ba3
--- /dev/null
+++ b/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs
@@ -0,0 +1,59 @@
+/* 
+ * Copyright 2020, 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;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace OptimizelySDK.OptimizelyDecisions
+{
+    public class DefaultDecisionReasons : IDecisionReasons
+    {
+        private List<string> Errors = new List<string>();
+        private List<String> Logs = new List<string>();
+
+        public static IDecisionReasons NewInstance(List<OptimizelyDecideOption> options)
+        {
+            if (options != null && options.Contains(OptimizelyDecideOption.INCLUDE_REASONS)) return new DefaultDecisionReasons();
+            else return new ErrorsDecisionReasons();
+        }
+
+        public static IDecisionReasons newInstance()
+        {
+            return NewInstance(null);
+        }
+
+        public void AddError(string format, params object[] args)
+        {
+            string message = string.Format(format, args);
+            Errors.Add(message);
+        }
+
+        public string AddInfo(string format, params object[] args)
+        {
+            string message = string.Format(format, args);
+            Logs.Add(message);
+            return message;
+        }
+
+        public List<string> ToReport()
+        {
+            List<string> reasons = new List<string>(Errors);
+            reasons.Concat(Logs);
+            return reasons;
+        }
+    }
+}
diff --git a/OptimizelySDK/OptimizelyDecisions/ErrorsDecisionReasons.cs b/OptimizelySDK/OptimizelyDecisions/ErrorsDecisionReasons.cs
new file mode 100644
index 00000000..890416ea
--- /dev/null
+++ b/OptimizelySDK/OptimizelyDecisions/ErrorsDecisionReasons.cs
@@ -0,0 +1,42 @@
+/* 
+ * Copyright 2020, 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;
+
+namespace OptimizelySDK.OptimizelyDecisions
+{
+    public class ErrorsDecisionReasons : IDecisionReasons
+    {
+        private readonly List<string> errors = new List<string>();
+
+        public void AddError(string format, params object[] args)
+        {
+            string message = string.Format(format, args);
+            errors.Add(message);
+        }
+
+        public string AddInfo(string format, params object[] args)
+        {
+            // skip tracking and pass-through reasons other than critical errors.
+            return string.Format(format, args);
+        }
+
+        public List<string> ToReport()
+        {
+            return errors;
+        }
+    }
+}
diff --git a/OptimizelySDK/OptimizelyDecisions/OptimizelyDecideOption.cs b/OptimizelySDK/OptimizelyDecisions/OptimizelyDecideOption.cs
new file mode 100644
index 00000000..bdf13915
--- /dev/null
+++ b/OptimizelySDK/OptimizelyDecisions/OptimizelyDecideOption.cs
@@ -0,0 +1,27 @@
+/* 
+ * Copyright 2020, 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.
+ */
+
+namespace OptimizelySDK.OptimizelyDecisions
+{
+    public enum OptimizelyDecideOption
+    {
+        DISABLE_DECISION_EVENT,
+        ENABLED_FLAGS_ONLY,
+        IGNORE_USER_PROFILE_SERVICE,
+        INCLUDE_REASONS,
+        EXCLUDE_VARIABLES
+    }
+}
diff --git a/OptimizelySDK/OptimizelyDecisions/OptimizelyDecision.cs b/OptimizelySDK/OptimizelyDecisions/OptimizelyDecision.cs
new file mode 100644
index 00000000..a0610d02
--- /dev/null
+++ b/OptimizelySDK/OptimizelyDecisions/OptimizelyDecision.cs
@@ -0,0 +1,66 @@
+/* 
+ * Copyright 2020, 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 OptimizelySDK.ErrorHandler;
+using OptimizelySDK.Logger;
+using System.Collections.Generic;
+
+namespace OptimizelySDK.OptimizelyDecisions
+{
+    public class OptimizelyDecision
+    {
+        private string VariationKey { get; set; }
+        private bool Enabled { get; set; }
+        private OptimizelyJSON Variables { get; set; }
+        private string RuleKey { get; set; }
+        private string FlagKey { get; set; }
+        private OptimizelyUserContext UserContext { get; set; }
+        private List<string> Reasons { get; set; }
+
+        public OptimizelyDecision(string variationKey,
+                              bool enabled,
+                              OptimizelyJSON variables,
+                              string ruleKey,
+                              string flagKey,
+                              OptimizelyUserContext userContext,
+                              List<string> reasons)
+        {
+            VariationKey = variationKey;
+            Enabled = enabled;
+            Variables = variables;
+            RuleKey = ruleKey;
+            FlagKey = flagKey;
+            UserContext = userContext;
+            Reasons = reasons;
+        }
+
+        public static OptimizelyDecision NewErrorDecision(string key,
+            OptimizelyUserContext optimizelyUserContext,
+            string error,
+            IErrorHandler errorHandler,
+            ILogger logger)
+        {
+            return new OptimizelyDecision(
+                null,
+                false,
+                new OptimizelyJSON(new Dictionary<string, object>(), errorHandler, logger),
+                null,
+                key,
+                optimizelyUserContext,
+                new List<string>() { error });
+        }
+    }
+}
diff --git a/OptimizelySDK/OptimizelyUserContext.cs b/OptimizelySDK/OptimizelyUserContext.cs
new file mode 100644
index 00000000..8bc2761a
--- /dev/null
+++ b/OptimizelySDK/OptimizelyUserContext.cs
@@ -0,0 +1,130 @@
+/* 
+ * Copyright 2020, 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 OptimizelySDK.Logger;
+using System.Collections.Generic;
+using OptimizelySDK.ErrorHandler;
+using OptimizelySDK.Entity;
+using OptimizelySDK.OptimizelyDecisions;
+
+namespace OptimizelySDK
+{
+    public class OptimizelyUserContext
+    {
+        private ILogger Logger;
+        private IErrorHandler ErrorHandler;
+        private string UserId { get; set; }
+        private UserAttributes UserAttributes { get; set; }
+        private Optimizely Optimizely { get; set; }
+
+        public OptimizelyUserContext(Optimizely optimizely, string userId, UserAttributes userAttributes, IErrorHandler errorHandler, ILogger logger)
+        {
+            ErrorHandler = errorHandler;
+            Logger = logger;
+            Optimizely = optimizely;
+            UserAttributes = userAttributes;
+            UserId = userId;
+        }
+
+        /// <summary>
+        /// Set an attribute for a given key.
+        /// </summary>
+        /// <param name="key">An attribute key</param>
+        /// <param name="value">value An attribute value</param>
+        public void SetAttribute(string key, object value)
+        {
+            UserAttributes.Add(key, value);
+        }
+
+        /// <summary>
+        /// Returns a decision result ({@link OptimizelyDecision}) for a given flag key and a user context, which contains all data required to deliver the flag.
+        /// <ul>
+        /// <li>If the SDK finds an error, it’ll return a decision with <b>null</b> for <b>variationKey</b>. The decision will include an error message in <b>reasons</b>.
+        /// </ul>
+        /// </summary>
+        /// <param name="key">A flag key for which a decision will be made.</param>
+        /// <returns>A decision result.</returns>
+        public OptimizelyDecision Decide(string key)
+        {
+            return Decide(key, new List<OptimizelyDecideOption>());
+        }
+
+        /// <summary>
+        /// Returns a decision result ({@link OptimizelyDecision}) for a given flag key and a user context, which contains all data required to deliver the flag.
+        /// <ul>
+        /// <li>If the SDK finds an error, it’ll return a decision with <b>null</b> for <b>variationKey</b>. The decision will include an error message in <b>reasons</b>.
+        /// </ul>
+        /// </summary>
+        /// <param name="key">A flag key for which a decision will be made.</param>
+        /// <param name="options">A list of options for decision-making.</param>
+        /// <returns>A decision result.</returns>
+        public OptimizelyDecision Decide(string key,
+            List<OptimizelyDecideOption> options)
+        {
+            return null;
+        }
+ 
+        /// <summary>
+        /// Returns a key-map of decision results for multiple flag keys and a user context.
+        /// </summary>
+        /// <param name="keys">list of flag keys for which a decision will be made.</param>
+        /// <returns>A dictionary of all decision results, mapped by flag keys.</returns>
+        public Dictionary<string, OptimizelyDecision> DecideForKeys(List<string> keys)
+        {
+            return null;
+        }
+
+        /// <summary>
+        /// Returns a key-map of decision results ({@link OptimizelyDecision}) for all active flag keys.
+        /// </summary>
+        /// <returns>A dictionary of all decision results, mapped by flag keys.</returns>
+        public Dictionary<string, OptimizelyDecision> DecideAll()
+        {
+            return DecideAll(new List<OptimizelyDecideOption>());
+        }
+
+
+        /// <summary>
+        /// Returns a key-map of decision results ({@link OptimizelyDecision}) for all active flag keys.
+        /// </summary>
+        /// <param name="options">A list of options for decision-making.</param>
+        /// <returns>All decision results mapped by flag keys.</returns>
+        public Dictionary<string, OptimizelyDecision> DecideAll(List<OptimizelyDecideOption> options)
+        {
+            return null;
+        }
+
+        /// <summary>
+        /// Track an event.
+        /// </summary>
+        /// <param name="eventName">The event name.</param>
+        public void TrackEvent(string eventName)
+        {
+            TrackEvent(eventName, new EventTags());
+        }
+
+        /// <summary>
+        /// Track an event.
+        /// </summary>
+        /// <param name="eventName">The event name.</param>
+        /// <param name="eventTags">A map of event tag names to event tag values.</param>
+        public void TrackEvent(string eventName, 
+            EventTags eventTags)
+        {
+            Optimizely.Track(eventName, UserId, UserAttributes, eventTags);
+        }
+    }
+}

From 35c5e76d087495df92ae2af4b16c1d2b85d457d6 Mon Sep 17 00:00:00 2001
From: muhammadnoman <muhammadnoman@folio3.com>
Date: Fri, 6 Nov 2020 21:07:11 +0500
Subject: [PATCH 02/19] Added DecisionTests

---
 .../OptimizelySDK.Net40.csproj                | 21 ++++++
 .../OptimizelySDK.NetStandard16.csproj        |  7 ++
 .../OptimizelySDK.NetStandard20.csproj        | 21 ++++++
 .../OptimizelyDecisionTest.cs                 | 69 +++++++++++++++++++
 .../OptimizelySDK.Tests.csproj                |  1 +
 ...DecisionReasons.cs => IDecisionReasons.cs} |  0
 .../OptimizelyDecisions/OptimizelyDecision.cs | 14 ++--
 OptimizelySDK/OptimizelySDK.csproj            |  7 ++
 8 files changed, 133 insertions(+), 7 deletions(-)
 create mode 100644 OptimizelySDK.Tests/OptimizelyDecisions/OptimizelyDecisionTest.cs
 rename OptimizelySDK/OptimizelyDecisions/{DecisionReasons.cs => IDecisionReasons.cs} (100%)

diff --git a/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj b/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj
index 5ce0521a..6afe938c 100644
--- a/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj
+++ b/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj
@@ -318,6 +318,27 @@
     </Compile>
 	<Compile Include="..\OptimizelySDK\OptlyConfig\IOptimizelyConfigManager.cs">
       <Link>OptlyConfig\IOptimizelyConfigManager.cs</Link>
+    </Compile>
+	<Compile Include="..\OptimizelySDK\OptimizelyDecisions\DecisionMessage.cs">
+      <Link>OptimizelyDecisions\DecisionMessage.cs</Link>
+    </Compile>
+	<Compile Include="..\OptimizelySDK\OptimizelyDecisions\IDecisionReasons.cs">
+      <Link>OptimizelyDecisions\IDecisionReasons.cs</Link>
+    </Compile>
+	<Compile Include="..\OptimizelySDK\OptimizelyDecisions\DefaultDecisionReasons.cs">
+      <Link>OptimizelyDecisions\DefaultDecisionReasons.cs</Link>
+    </Compile>
+	<Compile Include="..\OptimizelySDK\OptimizelyDecisions\ErrorsDecisionReasons.cs">
+      <Link>OptimizelyDecisions\ErrorsDecisionReasons.cs</Link>
+    </Compile>
+	<Compile Include="..\OptimizelySDK\OptimizelyDecisions\OptimizelyDecideOption.cs">
+      <Link>OptimizelyDecisions\OptimizelyDecideOption.cs</Link>
+    </Compile>
+	<Compile Include="..\OptimizelySDK\OptimizelyDecisions\OptimizelyDecision.cs">
+      <Link>OptimizelyDecisions\OptimizelyDecision.cs</Link>
+    </Compile>
+	<Compile Include="..\OptimizelySDK\OptimizelyUserContext.cs">
+      <Link>OptimizelyUserContext.cs</Link>
     </Compile>
   </ItemGroup>
   <ItemGroup>
diff --git a/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj b/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj
index 868c1d8c..0e164a97 100644
--- a/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj
+++ b/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj
@@ -88,6 +88,13 @@
 		<Compile Include="..\OptimizelySDK\OptlyConfig\OptimizelyVariation.cs" />
 		<Compile Include="..\OptimizelySDK\OptlyConfig\OptimizelyConfigService.cs" />
 		<Compile Include="..\OptimizelySDK\OptlyConfig\IOptimizelyConfigManager.cs" />
+		<Compile Include="..\OptimizelySDK\OptimizelyDecisions\DecisionMessage.cs" />
+		<Compile Include="..\OptimizelySDK\OptimizelyDecisions\IDecisionReasons.cs" />
+		<Compile Include="..\OptimizelySDK\OptimizelyDecisions\DefaultDecisionReasons.cs" />
+		<Compile Include="..\OptimizelySDK\OptimizelyDecisions\ErrorsDecisionReasons.cs"/>
+		<Compile Include="..\OptimizelySDK\OptimizelyDecisions\OptimizelyDecideOption.cs" />
+		<Compile Include="..\OptimizelySDK\OptimizelyDecisions\OptimizelyDecision.cs" />
+		<Compile Include="..\OptimizelySDK\OptimizelyUserContext.cs" />
 		<Compile Include="..\OptimizelySDK\Config\FallbackProjectConfigManager.cs">
 		  <Link>AtomicProjectConfigManager.cs</Link>
 		</Compile>
diff --git a/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj b/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj
index 1a126f2c..3ef5fe06 100644
--- a/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj
+++ b/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj
@@ -279,6 +279,27 @@
     </Compile>
 	<Compile Include="..\OptimizelySDK\OptlyConfig\IOptimizelyConfigManager.cs">
       <Link>OptlyConfig\IOptimizelyConfigManager.cs</Link>
+    </Compile>
+	<Compile Include="..\OptimizelySDK\OptimizelyDecisions\DecisionMessage.cs">
+      <Link>OptimizelyDecisions\DecisionMessage.cs</Link>
+    </Compile>
+	<Compile Include="..\OptimizelySDK\OptimizelyDecisions\IDecisionReasons.cs">
+      <Link>OptimizelyDecisions\IDecisionReasons.cs</Link>
+    </Compile>
+	<Compile Include="..\OptimizelySDK\OptimizelyDecisions\DefaultDecisionReasons.cs">
+      <Link>OptimizelyDecisions\DefaultDecisionReasons.cs</Link>
+    </Compile>
+	<Compile Include="..\OptimizelySDK\OptimizelyDecisions\ErrorsDecisionReasons.cs">
+      <Link>OptimizelyDecisions\ErrorsDecisionReasons.cs</Link>
+    </Compile>
+	<Compile Include="..\OptimizelySDK\OptimizelyDecisions\OptimizelyDecideOption.cs">
+      <Link>OptimizelyDecisions\OptimizelyDecideOption.cs</Link>
+    </Compile>
+	<Compile Include="..\OptimizelySDK\OptimizelyDecisions\OptimizelyDecision.cs">
+      <Link>OptimizelyDecisions\OptimizelyDecision.cs</Link>
+    </Compile>
+	<Compile Include="..\OptimizelySDK\OptimizelyUserContext.cs">
+      <Link>OptimizelyUserContext.cs</Link>
     </Compile>
   </ItemGroup>
   <ItemGroup>
diff --git a/OptimizelySDK.Tests/OptimizelyDecisions/OptimizelyDecisionTest.cs b/OptimizelySDK.Tests/OptimizelyDecisions/OptimizelyDecisionTest.cs
new file mode 100644
index 00000000..3e8d8c8f
--- /dev/null
+++ b/OptimizelySDK.Tests/OptimizelyDecisions/OptimizelyDecisionTest.cs
@@ -0,0 +1,69 @@
+using Moq;
+using NUnit.Framework;
+using OptimizelySDK.ErrorHandler;
+using OptimizelySDK.Logger;
+using OptimizelySDK.OptimizelyDecisions;
+using System;
+using System.Collections.Generic;
+
+namespace OptimizelySDK.Tests.OptimizelyDecisions
+{
+    [TestFixture]
+    public class OptimizelyDecisionTest
+    {
+        private Mock<ILogger> LoggerMock;
+        private Mock<IErrorHandler> ErrorHandlerMock;
+
+        [SetUp]
+        public void Initialize()
+        {
+            ErrorHandlerMock = new Mock<IErrorHandler>();
+            ErrorHandlerMock.Setup(e => e.HandleError(It.IsAny<Exception>()));
+
+            LoggerMock = new Mock<ILogger>();
+            LoggerMock.Setup(i => i.Log(It.IsAny<LogLevel>(), It.IsAny<string>()));
+        }
+        
+        [Test]
+        public void TestNewErrorDecision()
+        {
+            var optimizelyDecision = OptimizelyDecision.NewErrorDecision("var_key", null, "some error message", ErrorHandlerMock.Object, LoggerMock.Object);
+            Assert.IsNull(optimizelyDecision.VariationKey);
+            Assert.AreEqual(optimizelyDecision.FlagKey, "var_key");
+            Assert.AreEqual(optimizelyDecision.Variables.ToDictionary(), new Dictionary<string, object>());
+            Assert.AreEqual(optimizelyDecision.Reasons, new List<string>() { "some error message" });
+            Assert.IsNull(optimizelyDecision.RuleKey);
+            Assert.False(optimizelyDecision.Enabled);
+        }
+
+        [Test]
+        public void TestNewDecision()
+        {
+            var map = new Dictionary<string, object>() {
+                { "strField", "john doe" },
+                { "intField", 12 },
+                { "objectField", new Dictionary<string, object> () {
+                        { "inner_field_int", 3 }
+                    }
+                }
+            };
+            var optimizelyJSONUsingMap = new OptimizelyJSON(map, ErrorHandlerMock.Object, LoggerMock.Object);
+            string expectedStringObj = "{\"strField\":\"john doe\",\"intField\":12,\"objectField\":{\"inner_field_int\":3}}";
+
+            var optimizelyDecision = new OptimizelyDecision("var_key",
+                true,
+                optimizelyJSONUsingMap,
+                "experiment",
+                "feature_key",
+                null,
+                new List<string>());
+            Assert.AreEqual(optimizelyDecision.VariationKey, "var_key");
+            Assert.AreEqual(optimizelyDecision.FlagKey, "feature_key");
+            Assert.AreEqual(optimizelyDecision.Variables.ToString(), expectedStringObj);
+            Assert.AreEqual(optimizelyDecision.Reasons, new List<string>());
+            Assert.AreEqual(optimizelyDecision.RuleKey, "experiment");
+            Assert.True(optimizelyDecision.Enabled);
+        }
+
+    }
+}
diff --git a/OptimizelySDK.Tests/OptimizelySDK.Tests.csproj b/OptimizelySDK.Tests/OptimizelySDK.Tests.csproj
index 90bff004..0f44902c 100644
--- a/OptimizelySDK.Tests/OptimizelySDK.Tests.csproj
+++ b/OptimizelySDK.Tests/OptimizelySDK.Tests.csproj
@@ -78,6 +78,7 @@
     <Compile Include="DecisionServiceTest.cs" />
     <Compile Include="DefaultErrorHandlerTest.cs" />
     <Compile Include="EventTests\EventProcessorProps.cs" />
+    <Compile Include="OptimizelyDecisions\OptimizelyDecisionTest.cs" />
     <Compile Include="OptimizelyJSONTest.cs" />
     <Compile Include="EventTests\BatchEventProcessorTest.cs" />
     <Compile Include="EventTests\DefaultEventDispatcherTest.cs" />
diff --git a/OptimizelySDK/OptimizelyDecisions/DecisionReasons.cs b/OptimizelySDK/OptimizelyDecisions/IDecisionReasons.cs
similarity index 100%
rename from OptimizelySDK/OptimizelyDecisions/DecisionReasons.cs
rename to OptimizelySDK/OptimizelyDecisions/IDecisionReasons.cs
diff --git a/OptimizelySDK/OptimizelyDecisions/OptimizelyDecision.cs b/OptimizelySDK/OptimizelyDecisions/OptimizelyDecision.cs
index a0610d02..50334cf2 100644
--- a/OptimizelySDK/OptimizelyDecisions/OptimizelyDecision.cs
+++ b/OptimizelySDK/OptimizelyDecisions/OptimizelyDecision.cs
@@ -22,13 +22,13 @@ namespace OptimizelySDK.OptimizelyDecisions
 {
     public class OptimizelyDecision
     {
-        private string VariationKey { get; set; }
-        private bool Enabled { get; set; }
-        private OptimizelyJSON Variables { get; set; }
-        private string RuleKey { get; set; }
-        private string FlagKey { get; set; }
-        private OptimizelyUserContext UserContext { get; set; }
-        private List<string> Reasons { get; set; }
+        public string VariationKey { get; private set; }
+        public bool Enabled { get; private set; }
+        public OptimizelyJSON Variables { get; private set; }
+        public string RuleKey { get; private set; }
+        public string FlagKey { get; private set; }
+        public OptimizelyUserContext UserContext { get; private set; }
+        public List<string> Reasons { get; private set; }
 
         public OptimizelyDecision(string variationKey,
                               bool enabled,
diff --git a/OptimizelySDK/OptimizelySDK.csproj b/OptimizelySDK/OptimizelySDK.csproj
index 9964d942..4ea518da 100644
--- a/OptimizelySDK/OptimizelySDK.csproj
+++ b/OptimizelySDK/OptimizelySDK.csproj
@@ -87,6 +87,13 @@
     <Compile Include="Entity\Group.cs" />
     <Compile Include="Entity\IdKeyEntity.cs" />
     <Compile Include="Event\Entity\DecisionMetadata.cs" />
+    <Compile Include="OptimizelyDecisions\DecisionMessage.cs" />
+    <Compile Include="OptimizelyDecisions\IDecisionReasons.cs" />
+    <Compile Include="OptimizelyDecisions\DefaultDecisionReasons.cs" />
+    <Compile Include="OptimizelyDecisions\ErrorsDecisionReasons.cs" />
+    <Compile Include="OptimizelyDecisions\OptimizelyDecideOption.cs" />
+    <Compile Include="OptimizelyDecisions\OptimizelyDecision.cs" />
+    <Compile Include="OptimizelyUserContext.cs" />
     <Compile Include="OptimizelyJSON.cs" />
     <Compile Include="Entity\Rollout.cs" />
     <Compile Include="Entity\TrafficAllocation.cs" />

From f79974c976827faea40346dceb61fb66671f1b7a Mon Sep 17 00:00:00 2001
From: muhammadnoman <muhammadnoman@folio3.com>
Date: Fri, 6 Nov 2020 21:11:13 +0500
Subject: [PATCH 03/19] Added classes of optimizelyDecision in net35 file

---
 .../OptimizelySDK.Net35.csproj                | 21 +++++++++++++++++++
 1 file changed, 21 insertions(+)

diff --git a/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj b/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj
index e5730209..81cc70fc 100644
--- a/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj
+++ b/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj
@@ -302,6 +302,27 @@
 	<Compile Include="..\OptimizelySDK\OptlyConfig\IOptimizelyConfigManager.cs">
 	  <Link>OptlyConfig\IOptimizelyConfigManager.cs</Link>
 	</Compile>
+	<Compile Include="..\OptimizelySDK\OptimizelyDecisions\DecisionMessage.cs">
+      <Link>OptimizelyDecisions\DecisionMessage.cs</Link>
+    </Compile>
+	<Compile Include="..\OptimizelySDK\OptimizelyDecisions\IDecisionReasons.cs">
+      <Link>OptimizelyDecisions\IDecisionReasons.cs</Link>
+    </Compile>
+	<Compile Include="..\OptimizelySDK\OptimizelyDecisions\DefaultDecisionReasons.cs">
+      <Link>OptimizelyDecisions\DefaultDecisionReasons.cs</Link>
+    </Compile>
+	<Compile Include="..\OptimizelySDK\OptimizelyDecisions\ErrorsDecisionReasons.cs">
+      <Link>OptimizelyDecisions\ErrorsDecisionReasons.cs</Link>
+    </Compile>
+	<Compile Include="..\OptimizelySDK\OptimizelyDecisions\OptimizelyDecideOption.cs">
+      <Link>OptimizelyDecisions\OptimizelyDecideOption.cs</Link>
+    </Compile>
+	<Compile Include="..\OptimizelySDK\OptimizelyDecisions\OptimizelyDecision.cs">
+      <Link>OptimizelyDecisions\OptimizelyDecision.cs</Link>
+    </Compile>
+	<Compile Include="..\OptimizelySDK\OptimizelyUserContext.cs">
+      <Link>OptimizelyUserContext.cs</Link>
+    </Compile>
   </ItemGroup>
   <ItemGroup>
     <None Include="..\OptimizelySDK\Utils\schema.json">

From 7531f3c327a5f226d6a5ca93b31b6394f72b67ee Mon Sep 17 00:00:00 2001
From: muhammadnoman <muhammadnoman@folio3.com>
Date: Fri, 6 Nov 2020 22:28:14 +0500
Subject: [PATCH 04/19] Updated decisionmessage class and added decisionMessage
 test

---
 .../DecisionReasonsTest.cs                    | 40 +++++++++++++++++++
 .../OptimizelyDecisions/DecisionMessage.cs    | 11 ++---
 .../DefaultDecisionReasons.cs                 | 15 ++++---
 3 files changed, 54 insertions(+), 12 deletions(-)
 create mode 100644 OptimizelySDK.Tests/OptimizelyDecisions/DecisionReasonsTest.cs

diff --git a/OptimizelySDK.Tests/OptimizelyDecisions/DecisionReasonsTest.cs b/OptimizelySDK.Tests/OptimizelyDecisions/DecisionReasonsTest.cs
new file mode 100644
index 00000000..7e5ad975
--- /dev/null
+++ b/OptimizelySDK.Tests/OptimizelyDecisions/DecisionReasonsTest.cs
@@ -0,0 +1,40 @@
+/* 
+ * Copyright 2020, 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 NUnit.Framework;
+using OptimizelySDK.OptimizelyDecisions;
+using System.Collections.Generic;
+
+namespace OptimizelySDK.Tests.OptimizelyDecisions
+{
+    [TestFixture]
+    public class DecisionReasonsTest
+    {
+        
+        [Test]
+        public void TestNewDecisionReasonWith()
+        {
+            var decisionReasons = DefaultDecisionReasons.NewInstance(new List<OptimizelyDecideOption>() { OptimizelyDecideOption.INCLUDE_REASONS });
+            decisionReasons.AddError(DecisionMessage.Reason(DecisionMessage.FLAG_KEY_INVALID, "invalid_key"));
+            Assert.AreEqual(decisionReasons.ToReport()[0], "No flag was found for key \"invalid_key\".");
+            decisionReasons.AddError(DecisionMessage.Reason(DecisionMessage.VARIABLE_VALUE_INVALID, "invalid_key"));
+            Assert.AreEqual(decisionReasons.ToReport()[1], "Variable value for key \"invalid_key\" is invalid or wrong type.");
+          // decisionReasons.AddError(DecisionMessage.Reason(DecisionMessage.SDK_NOT_READY, ""));
+          // Assert.AreEqual(decisionReasons.ToReport()[2], "Optimizely SDK not configured properly yet.");
+        }
+
+    }
+}
diff --git a/OptimizelySDK/OptimizelyDecisions/DecisionMessage.cs b/OptimizelySDK/OptimizelyDecisions/DecisionMessage.cs
index ab2d1214..1f2c3301 100644
--- a/OptimizelySDK/OptimizelyDecisions/DecisionMessage.cs
+++ b/OptimizelySDK/OptimizelyDecisions/DecisionMessage.cs
@@ -19,14 +19,11 @@ namespace OptimizelySDK.OptimizelyDecisions
     public class DecisionMessage
     {
         public const string SDK_NOT_READY = "Optimizely SDK not configured properly yet.";
-        public const string FLAG_KEY_INVALID = "No flag was found for key \"%s\".";
-        public const string VARIABLE_VALUE_INVALID = "Variable value for key \"%s\" is invalid or wrong type.";
-
-        private string Format { get; set; }
-
-        public string Reason(params object[] args)
+        public const string FLAG_KEY_INVALID = "No flag was found for key \"{0}\".";
+        public const string VARIABLE_VALUE_INVALID = "Variable value for key \"{0}\" is invalid or wrong type.";
+        public static string Reason(string format, params object[] args)
         {
-            return string.Format(Format, args);
+            return string.Format(format, args);
         }
     }
 }
diff --git a/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs b/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs
index d7c48ba3..2e250c3f 100644
--- a/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs
+++ b/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs
@@ -14,7 +14,6 @@
  * limitations under the License.
  */
 
-using System;
 using System.Collections.Generic;
 using System.Linq;
 
@@ -23,15 +22,21 @@ namespace OptimizelySDK.OptimizelyDecisions
     public class DefaultDecisionReasons : IDecisionReasons
     {
         private List<string> Errors = new List<string>();
-        private List<String> Logs = new List<string>();
+        private List<string> Logs = new List<string>();
 
         public static IDecisionReasons NewInstance(List<OptimizelyDecideOption> options)
         {
-            if (options != null && options.Contains(OptimizelyDecideOption.INCLUDE_REASONS)) return new DefaultDecisionReasons();
-            else return new ErrorsDecisionReasons();
+            if (options != null && options.Contains(OptimizelyDecideOption.INCLUDE_REASONS))
+            {
+                return new DefaultDecisionReasons();
+            }
+            else
+            {
+                return new ErrorsDecisionReasons();
+            }
         }
 
-        public static IDecisionReasons newInstance()
+        public static IDecisionReasons NewInstance()
         {
             return NewInstance(null);
         }

From f006b080a0adbf6c9471f1cbb1b4b3167e88f629 Mon Sep 17 00:00:00 2001
From: muhammadnoman <muhammadnoman@folio3.com>
Date: Mon, 9 Nov 2020 19:14:51 +0500
Subject: [PATCH 05/19] Added OptimizelyDecisionTest

---
 .../DecisionReasonsTest.cs                    | 40 -------------------
 .../OptimizelyDecisionTest.cs                 | 30 +++++++++++++-
 .../DefaultDecisionReasons.cs                 |  2 +-
 3 files changed, 30 insertions(+), 42 deletions(-)
 delete mode 100644 OptimizelySDK.Tests/OptimizelyDecisions/DecisionReasonsTest.cs

diff --git a/OptimizelySDK.Tests/OptimizelyDecisions/DecisionReasonsTest.cs b/OptimizelySDK.Tests/OptimizelyDecisions/DecisionReasonsTest.cs
deleted file mode 100644
index 7e5ad975..00000000
--- a/OptimizelySDK.Tests/OptimizelyDecisions/DecisionReasonsTest.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-/* 
- * Copyright 2020, 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 NUnit.Framework;
-using OptimizelySDK.OptimizelyDecisions;
-using System.Collections.Generic;
-
-namespace OptimizelySDK.Tests.OptimizelyDecisions
-{
-    [TestFixture]
-    public class DecisionReasonsTest
-    {
-        
-        [Test]
-        public void TestNewDecisionReasonWith()
-        {
-            var decisionReasons = DefaultDecisionReasons.NewInstance(new List<OptimizelyDecideOption>() { OptimizelyDecideOption.INCLUDE_REASONS });
-            decisionReasons.AddError(DecisionMessage.Reason(DecisionMessage.FLAG_KEY_INVALID, "invalid_key"));
-            Assert.AreEqual(decisionReasons.ToReport()[0], "No flag was found for key \"invalid_key\".");
-            decisionReasons.AddError(DecisionMessage.Reason(DecisionMessage.VARIABLE_VALUE_INVALID, "invalid_key"));
-            Assert.AreEqual(decisionReasons.ToReport()[1], "Variable value for key \"invalid_key\" is invalid or wrong type.");
-          // decisionReasons.AddError(DecisionMessage.Reason(DecisionMessage.SDK_NOT_READY, ""));
-          // Assert.AreEqual(decisionReasons.ToReport()[2], "Optimizely SDK not configured properly yet.");
-        }
-
-    }
-}
diff --git a/OptimizelySDK.Tests/OptimizelyDecisions/OptimizelyDecisionTest.cs b/OptimizelySDK.Tests/OptimizelyDecisions/OptimizelyDecisionTest.cs
index 3e8d8c8f..b430e4ea 100644
--- a/OptimizelySDK.Tests/OptimizelyDecisions/OptimizelyDecisionTest.cs
+++ b/OptimizelySDK.Tests/OptimizelyDecisions/OptimizelyDecisionTest.cs
@@ -1,4 +1,20 @@
-using Moq;
+/* 
+ * Copyright 2020, 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 Moq;
 using NUnit.Framework;
 using OptimizelySDK.ErrorHandler;
 using OptimizelySDK.Logger;
@@ -65,5 +81,17 @@ public void TestNewDecision()
             Assert.True(optimizelyDecision.Enabled);
         }
 
+        [Test]
+        public void TestNewDecisionReasonWithDecideAllOptions()
+        {
+            var decisionReasons = DefaultDecisionReasons.NewInstance(new List<OptimizelyDecideOption>() { OptimizelyDecideOption.INCLUDE_REASONS });
+            decisionReasons.AddError(DecisionMessage.Reason(DecisionMessage.FLAG_KEY_INVALID, "invalid_key"));
+            Assert.AreEqual(decisionReasons.ToReport()[0], "No flag was found for key \"invalid_key\".");
+            decisionReasons.AddError(DecisionMessage.Reason(DecisionMessage.VARIABLE_VALUE_INVALID, "invalid_key"));
+            Assert.AreEqual(decisionReasons.ToReport()[1], "Variable value for key \"invalid_key\" is invalid or wrong type.");
+            decisionReasons.AddInfo("Some info message.");
+            Assert.AreEqual(decisionReasons.ToReport()[2], "Some info message.");
+        }
+
     }
 }
diff --git a/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs b/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs
index 2e250c3f..fe1a38a8 100644
--- a/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs
+++ b/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs
@@ -57,7 +57,7 @@ public string AddInfo(string format, params object[] args)
         public List<string> ToReport()
         {
             List<string> reasons = new List<string>(Errors);
-            reasons.Concat(Logs);
+            reasons.AddRange(Logs);
             return reasons;
         }
     }

From ac0c6a513d771affdf894cb54c63c02724c7d764 Mon Sep 17 00:00:00 2001
From: muhammadnoman <muhammadnoman@folio3.com>
Date: Mon, 9 Nov 2020 20:27:14 +0500
Subject: [PATCH 06/19] Added CreateUserContext in Optimizely

---
 OptimizelySDK.Tests/OptimizelyTest.cs  | 80 ++++++++++++++++++++++++++
 OptimizelySDK/IOptimizely.cs           |  9 +++
 OptimizelySDK/Optimizely.cs            | 36 +++++++++++-
 OptimizelySDK/OptimizelyUserContext.cs |  8 +--
 4 files changed, 126 insertions(+), 7 deletions(-)

diff --git a/OptimizelySDK.Tests/OptimizelyTest.cs b/OptimizelySDK.Tests/OptimizelyTest.cs
index 4b40e0de..e310571a 100644
--- a/OptimizelySDK.Tests/OptimizelyTest.cs
+++ b/OptimizelySDK.Tests/OptimizelyTest.cs
@@ -183,6 +183,86 @@ public PrivateObject CreatePrivateOptimizely()
         }
         #endregion
 
+        #region Test UserContext
+
+        [Test]
+        public void TestCreateUserContext()
+        {
+            var attribute = new UserAttributes
+                {
+                    { "device_type", "iPhone" },
+                    { "location", "San Francisco" }
+                };
+            var optlyUserContext = Optimizely.CreateUserContext(TestUserId, attribute);
+            Assert.AreEqual(TestUserId, optlyUserContext.UserId);
+            Assert.AreEqual(Optimizely, optlyUserContext.Optimizely);
+            Assert.AreEqual(attribute, optlyUserContext.UserAttributes);
+        }
+
+        [Test]
+        public void TestCreateUserContextWithoutAttributes()
+        {
+            var optlyUserContext = Optimizely.CreateUserContext(TestUserId);
+            Assert.AreEqual(TestUserId, optlyUserContext.UserId);
+            Assert.AreEqual(Optimizely, optlyUserContext.Optimizely);
+            Assert.IsTrue(optlyUserContext.UserAttributes.Count == 0);
+        }
+
+        [Test]
+        public void TestCreateUserContextMultipleAttribute()
+        {
+
+            var attribute1 = new UserAttributes
+                {
+                    { "device_type", "iPhone" },
+                    { "location", "San Francisco" }
+                };
+            var optlyUserContext1 = Optimizely.CreateUserContext("userId1", attribute1);
+            
+            var attribute2 = new UserAttributes
+                {
+                    { "device_type2", "Samsung" },
+                    { "location2", "California" }
+                };
+            var optlyUserContext2 = Optimizely.CreateUserContext("userId2", attribute2);
+
+            Assert.AreEqual("userId1", optlyUserContext1.UserId);
+            Assert.AreEqual(Optimizely, optlyUserContext1.Optimizely);
+            Assert.AreEqual(attribute1, optlyUserContext1.UserAttributes);
+
+            Assert.AreEqual("userId2", optlyUserContext2.UserId);
+            Assert.AreEqual(Optimizely, optlyUserContext2.Optimizely);
+            Assert.AreEqual(attribute2, optlyUserContext2.UserAttributes);
+        }
+
+        [Test]
+        public void TestChangeAttributeDoesNotEffectValues()
+        {
+            var userId = "testUserId";
+            var attribute = new UserAttributes
+                {
+                    { "device_type", "iPhone" },
+                    { "location", "San Francisco" }
+                };
+            var optlyUserContext = Optimizely.CreateUserContext(userId, attribute);
+            Assert.AreEqual(TestUserId, optlyUserContext.UserId);
+            Assert.AreEqual(Optimizely, optlyUserContext.Optimizely);
+            Assert.AreEqual(attribute, optlyUserContext.UserAttributes);
+
+            attribute = new UserAttributes
+                {
+                    { "device_type", "iPhone" },
+                    { "level", "low" },
+                    { "location", "San Francisco" }
+                };
+            userId = "InvalidUser";
+            Assert.AreEqual("testUserId", optlyUserContext.UserId);
+            Assert.AreEqual(Optimizely, optlyUserContext.Optimizely);
+            Assert.AreNotEqual(attribute, optlyUserContext.UserAttributes);
+        }
+
+        #endregion
+
         #region Test Validate
         [Test]
         public void TestInvalidInstanceLogMessages()
diff --git a/OptimizelySDK/IOptimizely.cs b/OptimizelySDK/IOptimizely.cs
index edd25e8a..836cdb92 100644
--- a/OptimizelySDK/IOptimizely.cs
+++ b/OptimizelySDK/IOptimizely.cs
@@ -34,6 +34,15 @@ public interface IOptimizely
 		/// <returns>null|Variation Representing variation</returns>
 		Variation Activate(string experimentKey, string userId, UserAttributes userAttributes = null);
 
+		/// <summary>
+		/// Create a context of the user for which decision APIs will be called.
+		/// A user context will be created successfully even when the SDK is not fully configured yet.
+		/// </summary>
+		/// <param name="userId">The user ID to be used for bucketing.</param>
+		/// <param name="userAttributes">The user's attributes</param>
+		/// <returns>OptimizelyUserContext | An OptimizelyUserContext associated with this OptimizelyClient.</returns>
+		OptimizelyUserContext CreateUserContext(string userId, UserAttributes userAttributes = null);
+
 		/// <summary>
 		/// Sends conversion event to Optimizely.
 		/// </summary>
diff --git a/OptimizelySDK/Optimizely.cs b/OptimizelySDK/Optimizely.cs
index cfc1a186..b30d00ac 100644
--- a/OptimizelySDK/Optimizely.cs
+++ b/OptimizelySDK/Optimizely.cs
@@ -29,6 +29,7 @@
 using OptimizelySDK.Event;
 using OptimizelySDK.OptlyConfig;
 using System.Net;
+using OptimizelySDK.OptimizelyDecisions;
 
 namespace OptimizelySDK
 {
@@ -59,6 +60,8 @@ public class Optimizely : IOptimizely, IDisposable
 
         private EventProcessor EventProcessor;
 
+        private List<OptimizelyDecideOption> DefaultDecideOptions;
+
         /// <summary>
         /// It returns true if the ProjectConfig is valid otherwise false.
         /// Also, it may block execution if GetConfig() blocks execution to get ProjectConfig.
@@ -158,11 +161,12 @@ public Optimizely(ProjectConfigManager configManager,
                          ILogger logger = null,
                          IErrorHandler errorHandler = null,
                          UserProfileService userProfileService = null,
-                         EventProcessor eventProcessor = null)
+                         EventProcessor eventProcessor = null,
+                         List<OptimizelyDecideOption> defaultDecideOptions = null)
         {
             ProjectConfigManager = configManager;
 
-            InitializeComponents(eventDispatcher, logger, errorHandler, userProfileService, notificationCenter, eventProcessor);
+            InitializeComponents(eventDispatcher, logger, errorHandler, userProfileService, notificationCenter, eventProcessor, defaultDecideOptions);
         }
 
         private void InitializeComponents(IEventDispatcher eventDispatcher = null,
@@ -170,7 +174,8 @@ private void InitializeComponents(IEventDispatcher eventDispatcher = null,
                          IErrorHandler errorHandler = null,
                          UserProfileService userProfileService = null,
                          NotificationCenter notificationCenter = null,
-                         EventProcessor eventProcessor = null)
+                         EventProcessor eventProcessor = null, 
+                         List<OptimizelyDecideOption> defaultDecideOptions = null)
         {
             Logger = logger ?? new NoOpLogger();
             EventDispatcher = eventDispatcher ?? new DefaultEventDispatcher(Logger);
@@ -181,6 +186,7 @@ private void InitializeComponents(IEventDispatcher eventDispatcher = null,
             NotificationCenter = notificationCenter ?? new NotificationCenter(Logger);
             DecisionService = new DecisionService(Bucketer, ErrorHandler, userProfileService, Logger);
             EventProcessor = eventProcessor ?? new ForwardingEventProcessor(EventDispatcher, NotificationCenter, Logger);
+            DefaultDecideOptions = defaultDecideOptions ?? new List<OptimizelyDecideOption>();
         }
 
         /// <summary>
@@ -682,6 +688,30 @@ public OptimizelyJSON GetFeatureVariableJSON(string featureKey, string variableK
             return GetFeatureVariableValueForType<OptimizelyJSON>(featureKey, variableKey, userId, userAttributes, FeatureVariable.JSON_TYPE);
         }
 
+        //============ decide ============//
+
+        /// <summary>
+        /// Create a context of the user for which decision APIs will be called.
+        /// A user context will be created successfully even when the SDK is not fully configured yet.
+        /// </summary>
+        /// <param name="userId">The user ID to be used for bucketing.</param>
+        /// <param name="userAttributes">The user's attributes</param>
+        /// <returns>OptimizelyUserContext | An OptimizelyUserContext associated with this OptimizelyClient.</returns>
+        public OptimizelyUserContext CreateUserContext(string userId,
+                                                       UserAttributes userAttributes = null)
+        {
+            var inputValues = new Dictionary<string, string>
+            {
+                { USER_ID, userId },
+            };
+
+            if (!ValidateStringInputs(inputValues))
+                return null;
+
+
+            return new OptimizelyUserContext(this, userId, userAttributes, ErrorHandler, Logger);
+        }
+
         /// <summary>
         /// Sends impression event.
         /// </summary>
diff --git a/OptimizelySDK/OptimizelyUserContext.cs b/OptimizelySDK/OptimizelyUserContext.cs
index 8bc2761a..e2621f20 100644
--- a/OptimizelySDK/OptimizelyUserContext.cs
+++ b/OptimizelySDK/OptimizelyUserContext.cs
@@ -26,16 +26,16 @@ public class OptimizelyUserContext
     {
         private ILogger Logger;
         private IErrorHandler ErrorHandler;
-        private string UserId { get; set; }
-        private UserAttributes UserAttributes { get; set; }
-        private Optimizely Optimizely { get; set; }
+        public string UserId { get; }
+        public UserAttributes UserAttributes { get; }
+        public Optimizely Optimizely { get; }
 
         public OptimizelyUserContext(Optimizely optimizely, string userId, UserAttributes userAttributes, IErrorHandler errorHandler, ILogger logger)
         {
             ErrorHandler = errorHandler;
             Logger = logger;
             Optimizely = optimizely;
-            UserAttributes = userAttributes;
+            UserAttributes = userAttributes ?? new UserAttributes();
             UserId = userId;
         }
 

From 6c9ac4d72c9a7a0cdf9539b70caed58145bc76a5 Mon Sep 17 00:00:00 2001
From: muhammadnoman <muhammadnoman@folio3.com>
Date: Wed, 11 Nov 2020 22:48:46 +0500
Subject: [PATCH 07/19] Added UserContext add reason logs BugFix of
 userAttributes Unit tests fix

---
 OptimizelySDK.Tests/BucketerTest.cs           |  23 +-
 OptimizelySDK.Tests/DecisionServiceTest.cs    |  93 ++++----
 .../OptimizelyUserContextTest.cs              | 201 ++++++++++++++++++
 OptimizelySDK/Bucketing/Bucketer.cs           |  14 +-
 OptimizelySDK/Bucketing/DecisionService.cs    | 177 +++++++++++----
 OptimizelySDK/Optimizely.cs                   | 199 ++++++++++++++---
 OptimizelySDK/OptimizelyUserContext.cs        |   4 +-
 OptimizelySDK/Utils/ExperimentUtils.cs        |  23 +-
 8 files changed, 590 insertions(+), 144 deletions(-)
 create mode 100644 OptimizelySDK.Tests/OptimizelyUserContextTest.cs

diff --git a/OptimizelySDK.Tests/BucketerTest.cs b/OptimizelySDK.Tests/BucketerTest.cs
index aeeeb96c..78b0feec 100644
--- a/OptimizelySDK.Tests/BucketerTest.cs
+++ b/OptimizelySDK.Tests/BucketerTest.cs
@@ -19,6 +19,7 @@
 using OptimizelySDK.Config;
 using OptimizelySDK.Entity;
 using OptimizelySDK.Logger;
+using OptimizelySDK.OptimizelyDecisions;
 
 namespace OptimizelySDK.Tests
 {
@@ -98,7 +99,7 @@ public void TestBucketValidExperimentNotInGroup()
 
             // control
             Assert.AreEqual(new Variation { Id = "7722370027", Key = "control" },
-                bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId));
+                bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance()));
 
             LoggerMock.Verify(l => l.Log(It.IsAny<LogLevel>(), It.IsAny<string>()), Times.Exactly(2));
             LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [3000] to user [testUserId] with bucketing ID [testBucketingIdControl!]."));
@@ -106,7 +107,7 @@ public void TestBucketValidExperimentNotInGroup()
 
             // variation
             Assert.AreEqual(new Variation { Id = "7721010009", Key = "variation" },
-                bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId));
+                bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance()));
 
             LoggerMock.Verify(l => l.Log(It.IsAny<LogLevel>(), It.IsAny<string>()), Times.Exactly(4));
             LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [7000] to user [testUserId] with bucketing ID [testBucketingIdControl!]."));
@@ -114,7 +115,7 @@ public void TestBucketValidExperimentNotInGroup()
 
             // no variation
             Assert.AreEqual(new Variation { },
-                bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId));
+                bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance()));
 
             LoggerMock.Verify(l => l.Log(It.IsAny<LogLevel>(), It.IsAny<string>()), Times.Exactly(6));
             LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [9000] to user [testUserId] with bucketing ID [testBucketingIdControl!]."));
@@ -130,7 +131,7 @@ public void TestBucketValidExperimentInGroup()
             // variation 1
             bucketer.SetBucketValues(new[] { 1000, 4000 });
             Assert.AreEqual(new Variation { Id = "7722260071", Key = "group_exp_1_var_1" },
-                bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId));
+                bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance()));
             LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [1000] to user [testUserId] with bucketing ID [testBucketingIdControl!]."));
             LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [testUserId] is in experiment [group_experiment_1] of group [7722400015]."));
             LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [4000] to user [testUserId] with bucketing ID [testBucketingIdControl!]."));
@@ -139,7 +140,7 @@ public void TestBucketValidExperimentInGroup()
             // variation 2
             bucketer.SetBucketValues(new[] { 1500, 7000 });
             Assert.AreEqual(new Variation { Id = "7722360022", Key = "group_exp_1_var_2" },
-                bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId));
+                bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance()));
             LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [1500] to user [testUserId] with bucketing ID [testBucketingIdControl!]."));
             LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [testUserId] is in experiment [group_experiment_1] of group [7722400015]."));
             LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [7000] to user [testUserId] with bucketing ID [testBucketingIdControl!]."));
@@ -148,7 +149,7 @@ public void TestBucketValidExperimentInGroup()
             // User not in experiment
             bucketer.SetBucketValues(new[] { 5000, 7000 });
             Assert.AreEqual(new Variation { },
-                bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId));
+                bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance()));
             LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [5000] to user [testUserId] with bucketing ID [testBucketingIdControl!]."));
             LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [testUserId] is not in experiment [group_experiment_1] of group [7722400015]."));
 
@@ -161,7 +162,7 @@ public void TestBucketInvalidExperiment()
             var bucketer = new Bucketer(LoggerMock.Object);
 
             Assert.AreEqual(new Variation { },
-                bucketer.Bucket(Config, new Experiment(), TestBucketingIdControl, TestUserId));
+                bucketer.Bucket(Config, new Experiment(), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance()));
 
             LoggerMock.Verify(l => l.Log(It.IsAny<LogLevel>(), It.IsAny<string>()), Times.Never);
         }
@@ -176,7 +177,7 @@ public void TestBucketWithBucketingId()
 
             // make sure that the bucketing ID is used for the variation bucketing and not the user ID
             Assert.AreEqual(expectedVariation, 
-                bucketer.Bucket(Config, experiment, TestBucketingIdControl, TestUserIdBucketsToVariation));
+                bucketer.Bucket(Config, experiment, TestBucketingIdControl, TestUserIdBucketsToVariation, DefaultDecisionReasons.NewInstance()));
         }
 
         // Test for invalid experiment keys, null variation should be returned
@@ -187,7 +188,7 @@ public void TestBucketVariationInvalidExperimentsWithBucketingId()
             var expectedVariation = new Variation();
 
             Assert.AreEqual(expectedVariation, 
-                bucketer.Bucket(Config, Config.GetExperimentFromKey("invalid_experiment"), TestBucketingIdVariation, TestUserId));
+                bucketer.Bucket(Config, Config.GetExperimentFromKey("invalid_experiment"), TestBucketingIdVariation, TestUserId, DefaultDecisionReasons.NewInstance()));
         }
 
         // Make sure that the bucketing ID is used to bucket the user into a group and not the user ID
@@ -200,7 +201,7 @@ public void TestBucketVariationGroupedExperimentsWithBucketingId()
 
             Assert.AreEqual(expectedGroupVariation,
                 bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_2"), 
-                TestBucketingIdGroupExp2Var2, TestUserIdBucketsToNoGroup));
+                TestBucketingIdGroupExp2Var2, TestUserIdBucketsToNoGroup, DefaultDecisionReasons.NewInstance()));
         }
 
         // Make sure that user gets bucketed into the rollout rule.
@@ -213,7 +214,7 @@ public void TestBucketRolloutRule()
             var expectedVariation = Config.GetVariationFromId(rolloutRule.Key, "177773");
 
             Assert.True(TestData.CompareObjects(expectedVariation, 
-                bucketer.Bucket(Config, rolloutRule, "testBucketingId", TestUserId)));
+                bucketer.Bucket(Config, rolloutRule, "testBucketingId", TestUserId, DefaultDecisionReasons.NewInstance())));
         }
     }
 }
\ No newline at end of file
diff --git a/OptimizelySDK.Tests/DecisionServiceTest.cs b/OptimizelySDK.Tests/DecisionServiceTest.cs
index 0936a857..3c83b3db 100644
--- a/OptimizelySDK.Tests/DecisionServiceTest.cs
+++ b/OptimizelySDK.Tests/DecisionServiceTest.cs
@@ -24,6 +24,7 @@
 using OptimizelySDK.Bucketing;
 using OptimizelySDK.Utils;
 using OptimizelySDK.Config;
+using OptimizelySDK.OptimizelyDecisions;
 
 namespace OptimizelySDK.Tests
 {
@@ -84,7 +85,7 @@ public void TestGetVariationForcedVariationPrecedesAudienceEval()
             LoggerMock.Verify(l => l.Log(LogLevel.INFO, string.Format("User \"{0}\" is forced in variation \"vtag5\".", WhitelistedUserId)), Times.Once);
             // no attributes provided for a experiment that has an audience
             Assert.IsTrue(TestData.CompareObjects(actualVariation, expectedVariation));
-            BucketerMock.Verify(_ => _.Bucket(It.IsAny<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never);
+            BucketerMock.Verify(_ => _.Bucket(It.IsAny<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>()), Times.Never);
         }
 
         [Test]
@@ -111,7 +112,7 @@ public void TestGetVariationEvaluatesUserProfileBeforeAudienceTargeting()
             // ensure that a user with a saved user profile, sees the same variation regardless of audience evaluation
             decisionService.GetVariation(experiment, UserProfileId, ProjectConfig, new UserAttributes());
 
-            BucketerMock.Verify(_ => _.Bucket(It.IsAny<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never);
+            BucketerMock.Verify(_ => _.Bucket(It.IsAny<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>()), Times.Never);
         }
 
         [Test]
@@ -123,7 +124,7 @@ public void TestGetForcedVariationReturnsForcedVariation()
             LoggerMock.Verify(l => l.Log(LogLevel.INFO, string.Format("User \"{0}\" is forced in variation \"{1}\".",
                 WhitelistedUserId, WhitelistedVariation.Key)), Times.Once);
 
-            BucketerMock.Verify(_ => _.Bucket(It.IsAny<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never);
+            BucketerMock.Verify(_ => _.Bucket(It.IsAny<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>()), Times.Never);
         }
 
         [Test]
@@ -164,7 +165,7 @@ public void TestGetForcedVariationWithInvalidVariation()
                 string.Format("Variation \"{0}\" is not in the datafile. Not activating user \"{1}\".", invalidVariationKey, userId)),
                 Times.Once);
 
-            BucketerMock.Verify(_ => _.Bucket(It.IsAny<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never);
+            BucketerMock.Verify(_ => _.Bucket(It.IsAny<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>()), Times.Never);
         }
 
         [Test]
@@ -216,7 +217,7 @@ public void TestGetStoredVariationLogsWhenLookupReturnsNull()
             DecisionService decisionService = new DecisionService(bucketer,
                  ErrorHandlerMock.Object, userProfileService, LoggerMock.Object);
 
-            Assert.IsNull(decisionService.GetStoredVariation(experiment, userProfile, ProjectConfig));
+            Assert.IsNull(decisionService.GetStoredVariation(experiment, userProfile, ProjectConfig, DefaultDecisionReasons.NewInstance()));
 
             LoggerMock.Verify(l => l.Log(LogLevel.INFO, string.Format("No previously activated variation of experiment \"{0}\" for user \"{1}\" found in user profile."
                 , experiment.Key, UserProfileId)), Times.Once);
@@ -241,7 +242,7 @@ public void TestGetStoredVariationReturnsNullWhenVariationIsNoLongerInConfig()
 
             DecisionService decisionService = new DecisionService(bucketer, ErrorHandlerMock.Object,
                 UserProfileServiceMock.Object, LoggerMock.Object);
-            Assert.IsNull(decisionService.GetStoredVariation(experiment, storedUserProfile, ProjectConfig));
+            Assert.IsNull(decisionService.GetStoredVariation(experiment, storedUserProfile, ProjectConfig, DefaultDecisionReasons.NewInstance()));
             LoggerMock.Verify(l => l.Log(LogLevel.INFO, string.Format("User \"{0}\" was previously bucketed into variation with ID \"{1}\" for experiment \"{2}\", but no matching variation was found for that user. We will re-bucket the user."
                 , UserProfileId, storedVariationId, experiment.Id)), Times.Once);
         }
@@ -265,7 +266,7 @@ public void TestGetVariationSavesBucketedVariationIntoUserProfile()
                 });
 
             var mockBucketer = new Mock<Bucketer>(LoggerMock.Object);
-            mockBucketer.Setup(m => m.Bucket(ProjectConfig, experiment, UserProfileId, UserProfileId)).Returns(variation);
+            mockBucketer.Setup(m => m.Bucket(ProjectConfig, experiment, UserProfileId, UserProfileId, It.IsAny<IDecisionReasons>())).Returns(variation);
 
             DecisionService decisionService = new DecisionService(mockBucketer.Object, ErrorHandlerMock.Object, UserProfileServiceMock.Object, LoggerMock.Object);
 
@@ -317,7 +318,7 @@ public void TestGetVariationSavesANewUserProfile()
             });
 
             var mockBucketer = new Mock<Bucketer>(LoggerMock.Object);
-            mockBucketer.Setup(m => m.Bucket(ProjectConfig, experiment, UserProfileId, UserProfileId)).Returns(variation);
+            mockBucketer.Setup(m => m.Bucket(ProjectConfig, experiment, UserProfileId, UserProfileId, It.IsAny<IDecisionReasons>())).Returns(variation);
 
             Dictionary<string, object> userProfile = null;
 
@@ -468,7 +469,7 @@ public void TestGetVariationWithBucketingId()
         public void TestGetVariationForFeatureExperimentGivenNullExperimentIds()
         {
             var featureFlag = ProjectConfig.GetFeatureFlagFromKey("empty_feature");
-            var decision = DecisionService.GetVariationForFeatureExperiment(featureFlag, GenericUserId, new UserAttributes() { }, ProjectConfig);
+            var decision = DecisionService.GetVariationForFeatureExperiment(featureFlag, GenericUserId, new UserAttributes() { }, ProjectConfig, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance());
 
             Assert.IsNull(decision);
 
@@ -487,7 +488,7 @@ public void TestGetVariationForFeatureExperimentGivenExperimentNotInDataFile()
                 ExperimentIds = new List<string> { "29039203" }
             };
 
-            var decision = DecisionService.GetVariationForFeatureExperiment(featureFlag, GenericUserId, new UserAttributes() { }, ProjectConfig);
+            var decision = DecisionService.GetVariationForFeatureExperiment(featureFlag, GenericUserId, new UserAttributes() { }, ProjectConfig, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance());
             Assert.IsNull(decision);
 
             LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Experiment ID \"29039203\" is not in datafile."));
@@ -502,7 +503,7 @@ public void TestGetVariationForFeatureExperimentGivenNonMutexGroupAndUserNotBuck
             DecisionServiceMock.Setup(ds => ds.GetVariation(multiVariateExp, "user1", ProjectConfig, null)).Returns<Variation>(null);
             var featureFlag = ProjectConfig.GetFeatureFlagFromKey("multi_variate_feature");
 
-            var decision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", new UserAttributes(), ProjectConfig);
+            var decision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", new UserAttributes(), ProjectConfig, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance());
             Assert.IsNull(decision);
 
             LoggerMock.Verify(l => l.Log(LogLevel.INFO, "The user \"user1\" is not bucketed into any of the experiments on the feature \"multi_variate_feature\"."));
@@ -518,10 +519,10 @@ public void TestGetVariationForFeatureExperimentGivenNonMutexGroupAndUserIsBucke
             var userAttributes = new UserAttributes();
 
             DecisionServiceMock.Setup(ds => ds.GetVariation(ProjectConfig.GetExperimentFromKey("test_experiment_multivariate"),
-                "user1", ProjectConfig, userAttributes)).Returns(variation);
+                "user1", ProjectConfig, userAttributes, It.IsAny<List<OptimizelyDecideOption>>(), It.IsAny<IDecisionReasons>())).Returns(variation);
 
             var featureFlag = ProjectConfig.GetFeatureFlagFromKey("multi_variate_feature");
-            var decision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig);
+            var decision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance());
 
             Assert.IsTrue(TestData.CompareObjects(expectedDecision, decision));
 
@@ -541,7 +542,7 @@ public void TestGetVariationForFeatureExperimentGivenMutexGroupAndUserIsBucketed
                 userAttributes)).Returns(variation);
 
             var featureFlag = ProjectConfig.GetFeatureFlagFromKey("boolean_feature");
-            var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig);
+            var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance());
 
             Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision));
 
@@ -553,11 +554,11 @@ public void TestGetVariationForFeatureExperimentGivenMutexGroupAndUserIsBucketed
         public void TestGetVariationForFeatureExperimentGivenMutexGroupAndUserNotBucketed()
         {
             var mutexExperiment = ProjectConfig.GetExperimentFromKey("group_experiment_1");
-            DecisionServiceMock.Setup(ds => ds.GetVariation(It.IsAny<Experiment>(), It.IsAny<string>(), ProjectConfig, It.IsAny<UserAttributes>())).
+            DecisionServiceMock.Setup(ds => ds.GetVariation(It.IsAny<Experiment>(), It.IsAny<string>(), ProjectConfig, It.IsAny<UserAttributes>(), It.IsAny<List<OptimizelyDecideOption>>(), It.IsAny<IDecisionReasons>())).
                 Returns<Variation>(null);
 
             var featureFlag = ProjectConfig.GetFeatureFlagFromKey("boolean_feature");
-            var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", new UserAttributes(), ProjectConfig);
+            var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", new UserAttributes(), ProjectConfig, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance());
 
             Assert.IsNull(actualDecision);
 
@@ -582,7 +583,7 @@ public void TestGetVariationForFeatureRolloutWhenNoRuleInRollouts()
 
             var decisionService = new DecisionService(new Bucketer(new NoOpLogger()), new NoOpErrorHandler(), null, new NoOpLogger());
 
-            var variation = decisionService.GetVariationForFeatureRollout(featureFlag, "userId1", null, projectConfig);
+            var variation = decisionService.GetVariationForFeatureRollout(featureFlag, "userId1", null, projectConfig, DefaultDecisionReasons.NewInstance());
 
             Assert.IsNull(variation);
         }
@@ -600,9 +601,9 @@ public void TestGetVariationForFeatureRolloutWhenRolloutIsNotInDataFile()
                 Variables = featureFlag.Variables
             };
 
-            DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny<FeatureFlag>(), It.IsAny<string>(), It.IsAny<UserAttributes>(), ProjectConfig)).Returns<Variation>(null);
+            DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny<FeatureFlag>(), It.IsAny<string>(), It.IsAny<UserAttributes>(), ProjectConfig, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance())).Returns<Variation>(null);
 
-            var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureRollout(featureFlag, "user1", new UserAttributes(), ProjectConfig);
+            var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureRollout(featureFlag, "user1", new UserAttributes(), ProjectConfig, DefaultDecisionReasons.NewInstance());
             Assert.IsNull(actualDecision);
 
             LoggerMock.Verify(l => l.Log(LogLevel.INFO, "The feature flag \"boolean_feature\" is not used in a rollout."));
@@ -624,10 +625,10 @@ public void TestGetVariationForFeatureRolloutWhenUserIsBucketedInTheTargetingRul
             };
 
             BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(),
-                It.IsAny<string>())).Returns(variation);
+                It.IsAny<string>(), It.IsAny<IDecisionReasons>())).Returns(variation);
             var decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, null, LoggerMock.Object);
 
-            var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", userAttributes, ProjectConfig);
+            var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", userAttributes, ProjectConfig, DefaultDecisionReasons.NewInstance());
             Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision));
         }
 
@@ -648,11 +649,11 @@ public void TestGetVariationForFeatureRolloutWhenUserIsNotBucketedInTheTargeting
                 { "browser_type", "chrome" }
             };
 
-            BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), experiment, It.IsAny<string>(), It.IsAny<string>())).Returns<Variation>(null);
-            BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), everyoneElseRule, It.IsAny<string>(), It.IsAny<string>())).Returns(variation);
+            BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), experiment, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>())).Returns<Variation>(null);
+            BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), everyoneElseRule, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>())).Returns(variation);
             var decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, null, LoggerMock.Object);
 
-            var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", userAttributes, ProjectConfig);
+            var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", userAttributes, ProjectConfig, DefaultDecisionReasons.NewInstance());
             Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision));
         }
 
@@ -668,10 +669,10 @@ public void TestGetVariationForFeatureRolloutWhenUserIsNeitherBucketedInTheTarge
                 { "browser_type", "chrome" }
             };
 
-            BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>())).Returns<Variation>(null);
+            BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>())).Returns<Variation>(null);
             var decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, null, LoggerMock.Object);
 
-            var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", userAttributes, ProjectConfig);
+            var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", userAttributes, ProjectConfig, DefaultDecisionReasons.NewInstance());
             Assert.IsNull(actualDecision);
         }
 
@@ -689,11 +690,11 @@ public void TestGetVariationForFeatureRolloutWhenUserDoesNotQualifyForAnyTargeti
             var variation = everyoneElseRule.Variations[0];
             var expectedDecision = new FeatureDecision(everyoneElseRule, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT);
 
-            BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), everyoneElseRule, It.IsAny<string>(), It.IsAny<string>())).Returns(variation);
+            BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), everyoneElseRule, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>())).Returns(variation);
             var decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, null, LoggerMock.Object);
 
             // Provide null attributes so that user does not qualify for audience.
-            var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", null, ProjectConfig);
+            var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", null, ProjectConfig, DefaultDecisionReasons.NewInstance());
 
             Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision));
 
@@ -723,7 +724,7 @@ public void TestGetVariationForFeatureRolloutAudienceAndTrafficeAllocationCheck(
             {
                 { "device_type", "iPhone" },
                 { "location", "San Francisco" }
-            }, ProjectConfig);
+            }, ProjectConfig, DefaultDecisionReasons.NewInstance());
 
             // Returned variation id should be '177773' because of audience 'iPhone users in San Francisco'.
             var expectedDecision = new FeatureDecision(expWithAudienceiPhoneUsers, varWithAudienceiPhoneUsers, FeatureDecision.DECISION_SOURCE_ROLLOUT);
@@ -733,7 +734,7 @@ public void TestGetVariationForFeatureRolloutAudienceAndTrafficeAllocationCheck(
             actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, new UserAttributes
             {
                 { "browser_type", "chrome" }
-            }, ProjectConfig);
+            }, ProjectConfig, DefaultDecisionReasons.NewInstance());
 
             // Returned variation id should be '177771' because of audience 'Chrome users'.
             expectedDecision = new FeatureDecision(expWithAudienceChromeUsers, varWithAudienceChromeUsers, FeatureDecision.DECISION_SOURCE_ROLLOUT);
@@ -741,7 +742,7 @@ public void TestGetVariationForFeatureRolloutAudienceAndTrafficeAllocationCheck(
 
             // Calling with no audience.
             mockBucketer.Setup(bm => bm.GenerateBucketValue(It.IsAny<string>())).Returns(8000);
-            actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, new UserAttributes(), ProjectConfig);
+            actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, new UserAttributes(), ProjectConfig, DefaultDecisionReasons.NewInstance());
 
             // Returned variation id should be of everyone else rule because of no audience.
             expectedDecision = new FeatureDecision(expWithNoAudience, varWithNoAudience, FeatureDecision.DECISION_SOURCE_ROLLOUT);
@@ -752,7 +753,7 @@ public void TestGetVariationForFeatureRolloutAudienceAndTrafficeAllocationCheck(
             actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, new UserAttributes
             {
                 { "browser_type", "chrome" }
-            }, ProjectConfig);
+            }, ProjectConfig, DefaultDecisionReasons.NewInstance());
 
             // Returned decision entity should be null because bucket value exceeds traffice allocation of everyone else rule.
             Assert.Null(actualDecision);
@@ -768,21 +769,21 @@ public void TestGetVariationForFeatureRolloutCheckAudienceInEveryoneElseRule()
             var variation = everyoneElseRule.Variations[0];
             var expectedDecision = new FeatureDecision(everyoneElseRule, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT);
 
-            BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), everyoneElseRule, It.IsAny<string>(), WhitelistedUserId)).Returns(variation);
-            BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), everyoneElseRule, It.IsAny<string>(), GenericUserId)).Returns<Variation>(null);
+            BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), everyoneElseRule, It.IsAny<string>(), WhitelistedUserId, It.IsAny<IDecisionReasons>())).Returns(variation);
+            BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), everyoneElseRule, It.IsAny<string>(), GenericUserId, It.IsAny<IDecisionReasons>())).Returns<Variation>(null);
             var decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, null, LoggerMock.Object);
 
             // Returned variation id should be of everyone else rule as it passes audience Id checking.
-            var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, WhitelistedUserId, null, ProjectConfig);
+            var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, WhitelistedUserId, null, ProjectConfig, DefaultDecisionReasons.NewInstance());
             Assert.True(TestData.CompareObjects(expectedDecision, actualDecision));
 
             // Returned variation id should be null.
-            actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, null, ProjectConfig);
+            actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, null, ProjectConfig, DefaultDecisionReasons.NewInstance());
             Assert.Null(actualDecision);
 
             // Returned variation id should be null as it fails audience Id checking.
             everyoneElseRule.AudienceIds = new string[] { ProjectConfig.Audiences[0].Id };
-            actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, null, ProjectConfig);
+            actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, null, ProjectConfig, DefaultDecisionReasons.NewInstance());
             Assert.Null(actualDecision);
 
             LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "User \"testUser1\" does not meet the conditions for targeting rule \"1\"."), Times.Once);
@@ -807,7 +808,7 @@ public void TestGetVariationForFeatureWhenTheUserIsBucketedIntoFeatureExperiment
             var expectedDecision = new FeatureDecision(expectedExperiment, variation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST);
 
             DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny<FeatureFlag>(), It.IsAny<string>(),
-                It.IsAny<UserAttributes>(), ProjectConfig)).Returns(expectedDecision);
+                It.IsAny<UserAttributes>(), ProjectConfig, It.IsAny<List<OptimizelyDecideOption>>(), It.IsAny<IDecisionReasons>())).Returns(expectedDecision);
 
             var actualDecision = DecisionServiceMock.Object.GetVariationForFeature(featureFlag, "user1", ProjectConfig, new UserAttributes());
             Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision));
@@ -826,9 +827,9 @@ public void TestGetVariationForFeatureWhenTheUserIsNotBucketedIntoFeatureExperim
             var expectedDecision = new FeatureDecision(expectedExperiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT);
 
             DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny<FeatureFlag>(), It.IsAny<string>(),
-                It.IsAny<UserAttributes>(), ProjectConfig)).Returns<Variation>(null);
+                It.IsAny<UserAttributes>(), ProjectConfig, It.IsAny<List<OptimizelyDecideOption>>(), It.IsAny<IDecisionReasons>())).Returns<Variation>(null);
             DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureRollout(It.IsAny<FeatureFlag>(), It.IsAny<string>(),
-                It.IsAny<UserAttributes>(), ProjectConfig)).Returns(expectedDecision);
+                It.IsAny<UserAttributes>(), ProjectConfig, It.IsAny<IDecisionReasons>())).Returns(expectedDecision);
 
             var actualDecision = DecisionServiceMock.Object.GetVariationForFeature(featureFlag, "user1", ProjectConfig, new UserAttributes());
 
@@ -844,8 +845,8 @@ public void TestGetVariationForFeatureWhenTheUserIsNeitherBucketedIntoFeatureExp
             var featureFlag = ProjectConfig.GetFeatureFlagFromKey("string_single_variable_feature");
             var expectedDecision = new FeatureDecision(null, null, FeatureDecision.DECISION_SOURCE_ROLLOUT);
 
-            DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny<FeatureFlag>(), It.IsAny<string>(), It.IsAny<UserAttributes>(), ProjectConfig)).Returns<Variation>(null);
-            DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureRollout(It.IsAny<FeatureFlag>(), It.IsAny<string>(), It.IsAny<UserAttributes>(), ProjectConfig)).Returns<Variation>(null);
+            DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny<FeatureFlag>(), It.IsAny<string>(), It.IsAny<UserAttributes>(), ProjectConfig, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance())).Returns<Variation>(null);
+            DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureRollout(It.IsAny<FeatureFlag>(), It.IsAny<string>(), It.IsAny<UserAttributes>(), ProjectConfig, DefaultDecisionReasons.NewInstance())).Returns<Variation>(null);
 
             var actualDecision = DecisionServiceMock.Object.GetVariationForFeature(featureFlag, "user1", ProjectConfig, new UserAttributes());
             Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision));
@@ -866,8 +867,8 @@ public void TestGetVariationForFeatureWhenTheUserIsBuckedtedInBothExperimentAndR
                 { "browser_type", "chrome" }
             };
 
-            DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, "user1", ProjectConfig, userAttributes)).Returns(variation);
-            var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig);
+            DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, "user1", ProjectConfig, userAttributes, It.IsAny<List<OptimizelyDecideOption>>(), It.IsAny<IDecisionReasons>())).Returns(variation);
+            var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance());
 
             // The user is bucketed into feature experiment's variation.
             Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision));
@@ -877,8 +878,8 @@ public void TestGetVariationForFeatureWhenTheUserIsBuckedtedInBothExperimentAndR
             var rolloutVariation = rolloutExperiment.Variations[0];
             var expectedRolloutDecision = new FeatureDecision(rolloutExperiment, rolloutVariation, FeatureDecision.DECISION_SOURCE_ROLLOUT);
 
-            BucketerMock.Setup(bm => bm.Bucket(ProjectConfig, rolloutExperiment, It.IsAny<string>(), It.IsAny<string>())).Returns(rolloutVariation);
-            var actualRolloutDecision = DecisionServiceMock.Object.GetVariationForFeatureRollout(featureFlag, "user1", userAttributes, ProjectConfig);
+            BucketerMock.Setup(bm => bm.Bucket(ProjectConfig, rolloutExperiment, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>())).Returns(rolloutVariation);
+            var actualRolloutDecision = DecisionServiceMock.Object.GetVariationForFeatureRollout(featureFlag, "user1", userAttributes, ProjectConfig, DefaultDecisionReasons.NewInstance());
 
             // The user is bucketed into feature rollout's variation.
 
diff --git a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs
new file mode 100644
index 00000000..029c8499
--- /dev/null
+++ b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs
@@ -0,0 +1,201 @@
+/**
+ *
+ *    Copyright 2020, Optimizely and contributors
+ *
+ *    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 Castle.Core.Internal;
+using Moq;
+using NUnit.Framework;
+using OptimizelySDK.Entity;
+using OptimizelySDK.ErrorHandler;
+using OptimizelySDK.Event.Dispatcher;
+using OptimizelySDK.Logger;
+using OptimizelySDK.OptimizelyDecisions;
+using System;
+using System.Collections.Generic;
+
+namespace OptimizelySDK.Tests
+{
+    [TestFixture]
+    public class OptimizelyUserContextTest
+    {
+        string UserID = "testUserID";
+        private Optimizely Optimizely;
+        private Mock<ILogger> LoggerMock;
+        private Mock<IErrorHandler> ErrorHandlerMock;
+        private Mock<IEventDispatcher> EventDispatcherMock;
+
+        [SetUp]
+        public void SetUp()
+        {
+            LoggerMock = new Mock<ILogger>();
+            LoggerMock.Setup(i => i.Log(It.IsAny<LogLevel>(), It.IsAny<string>()));
+
+            ErrorHandlerMock = new Mock<IErrorHandler>();
+            ErrorHandlerMock.Setup(e => e.HandleError(It.IsAny<Exception>()));
+            EventDispatcherMock = new Mock<IEventDispatcher>();
+
+            Optimizely = new Optimizely(TestData.Datafile, EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object);
+        }
+
+        [Test]
+        public void OptimizelyUserContext_withAttributes()
+        {
+            var attributes = new UserAttributes() { { "house", "GRYFFINDOR" } };
+            OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object);
+
+            Assert.AreEqual(user.Optimizely, Optimizely);
+            Assert.AreEqual(user.UserId, UserID);
+            Assert.AreEqual(user.UserAttributes, attributes);
+        }
+
+        [Test]
+        public void OptimizelyUserContext_noAttributes()
+        {
+            OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, null, ErrorHandlerMock.Object, LoggerMock.Object);
+
+            Assert.AreEqual(user.Optimizely, Optimizely);
+            Assert.AreEqual(user.UserId, UserID);
+            Assert.True(user.UserAttributes.Count == 0);
+        }
+
+        [Test]
+        public void SetAttribute()
+        {
+            var attributes = new UserAttributes() { { "house", "GRYFFINDOR" } };
+            OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object);
+
+            user.SetAttribute("k1", "v1");
+            user.SetAttribute("k2", true);
+            user.SetAttribute("k3", 100);
+            user.SetAttribute("k4", 3.5);
+
+            Assert.AreEqual(user.Optimizely, Optimizely);
+            Assert.AreEqual(user.UserId, UserID);
+            var newAttributes = user.UserAttributes;
+            Assert.AreEqual(newAttributes["house"], "GRYFFINDOR");
+            Assert.AreEqual(newAttributes["k1"], "v1");
+            Assert.AreEqual(newAttributes["k2"], true);
+            Assert.AreEqual(newAttributes["k3"], 100);
+            Assert.AreEqual(newAttributes["k4"], 3.5);
+        }
+
+        [Test]
+        public void SetAttribute_noAttribute()
+        {
+            OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, null, ErrorHandlerMock.Object, LoggerMock.Object);
+
+            user.SetAttribute("k1", "v1");
+            user.SetAttribute("k2", true);
+
+            Assert.AreEqual(user.Optimizely, Optimizely);
+            Assert.AreEqual(user.UserId, UserID);
+            var newAttributes = user.UserAttributes;
+            Assert.AreEqual(newAttributes["k1"], "v1");
+            Assert.AreEqual(newAttributes["k2"], true);
+        }
+
+        [Test]
+        public void SetAttribute_override()
+        {
+            var attributes = new UserAttributes() { { "house", "GRYFFINDOR" } };
+            OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object);
+
+            user.SetAttribute("k1", "v1");
+            user.SetAttribute("house", "v2");
+
+            var newAttributes = user.UserAttributes;
+            Assert.AreEqual(newAttributes["k1"], "v1");
+            Assert.AreEqual(newAttributes["house"], "v2");
+        }
+
+        [Test]
+        public void SetAttribute_nullValue()
+        {
+            var attributes = new UserAttributes() { { "k1", null } };
+            OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object);
+
+            var newAttributes = user.UserAttributes;
+            Assert.AreEqual(newAttributes["k1"], null);
+
+            user.SetAttribute("k1", true);
+            newAttributes = user.UserAttributes;
+            Assert.AreEqual(newAttributes["k1"], true);
+
+            user.SetAttribute("k1", null);
+            newAttributes = user.UserAttributes;
+            Assert.AreEqual(newAttributes["k1"], null);
+        }
+
+        #region decide
+
+        [Test]
+        public void Decide()
+        {
+            var flagKey = "multi_variate_feature";
+            var variablesExpected = Optimizely.GetAllFeatureVariables(flagKey, UserID);
+
+            var user = Optimizely.CreateUserContext(UserID);
+            user.SetAttribute("browser_type", "chrome");
+            var decision = user.Decide(flagKey);
+
+            Assert.AreEqual(decision.VariationKey, "Gred");
+            Assert.False(decision.Enabled);
+            Assert.AreEqual(decision.Variables.ToDictionary(), variablesExpected.ToDictionary());
+            Assert.AreEqual(decision.RuleKey, "test_experiment_multivariate");
+            Assert.AreEqual(decision.FlagKey, flagKey);
+            Assert.AreEqual(decision.UserContext, user);
+            Assert.True(decision.Reasons.IsNullOrEmpty());
+        }
+
+        [Test]
+        public void DecideInvalidFlagKey()
+        {
+            var flagKey = "invalid_feature";
+
+            var user = Optimizely.CreateUserContext(UserID);
+            user.SetAttribute("browser_type", "chrome");
+            var decision = user.Decide(flagKey);
+
+            Assert.Null(decision.VariationKey);
+            Assert.False(decision.Enabled);
+            Assert.AreEqual(decision.Variables.ToDictionary(), new Dictionary<string, object>());
+            Assert.IsNull(decision.RuleKey);
+            Assert.AreEqual(decision.FlagKey, flagKey);
+            Assert.AreEqual(decision.UserContext, user);
+            Assert.True(decision.Reasons.IsNullOrEmpty());
+        }
+
+        [Test]
+        public void DecideInvalidConfig()
+        {
+            Optimizely optimizely = new Optimizely(TestData.UnsupportedVersionDatafile, EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object);
+
+            var flagKey = "multi_variate_feature";
+            var decisionExpected = OptimizelyDecision.NewErrorDecision(
+                flagKey,
+                new OptimizelyUserContext(optimizely, UserID, new UserAttributes(), ErrorHandlerMock.Object, LoggerMock.Object),
+                DecisionMessage.SDK_NOT_READY,
+                ErrorHandlerMock.Object,
+                LoggerMock.Object);
+            var user = optimizely.CreateUserContext(UserID);
+            var decision = user.Decide(flagKey);
+
+            Assert.IsTrue(TestData.CompareObjects(decision, decisionExpected));
+        }
+        #endregion
+
+    }
+}
diff --git a/OptimizelySDK/Bucketing/Bucketer.cs b/OptimizelySDK/Bucketing/Bucketer.cs
index b8944687..be62727a 100644
--- a/OptimizelySDK/Bucketing/Bucketer.cs
+++ b/OptimizelySDK/Bucketing/Bucketer.cs
@@ -15,6 +15,7 @@
  */
 using OptimizelySDK.Entity;
 using OptimizelySDK.Logger;
+using OptimizelySDK.OptimizelyDecisions;
 using System;
 using System.Collections.Generic;
 using System.Text;
@@ -102,8 +103,9 @@ private string FindBucket(string bucketingId, string userId, string parentId, IE
         /// <param name="experiment">Experiment Experiment in which user is to be bucketed</param>
         /// <param name="bucketingId">A customer-assigned value used to create the key for the murmur hash.</param>
         /// <param name="userId">User identifier</param>
+        /// <param name="reasons">Decision log messages.</param>
         /// <returns>Variation which will be shown to the user</returns>
-        public virtual Variation Bucket(ProjectConfig config, Experiment experiment, string bucketingId, string userId)
+        public virtual Variation Bucket(ProjectConfig config, Experiment experiment, string bucketingId, string userId, IDecisionReasons reasons)
         {
             string message;
             Variation variation;
@@ -122,26 +124,26 @@ public virtual Variation Bucket(ProjectConfig config, Experiment experiment, str
                 if (string.IsNullOrEmpty(userExperimentId))
                 {
                     message = $"User [{userId}] is in no experiment.";
-                    Logger.Log(LogLevel.INFO, message);
+                    Logger.Log(LogLevel.INFO, reasons.AddInfo(message));
                     return new Variation();
                 }
 
                 if (userExperimentId != experiment.Id)
                 {
                     message = $"User [{userId}] is not in experiment [{experiment.Key}] of group [{experiment.GroupId}].";
-                    Logger.Log(LogLevel.INFO, message);
+                    Logger.Log(LogLevel.INFO, reasons.AddInfo(message));
                     return new Variation();
                 }
 
                 message = $"User [{userId}] is in experiment [{experiment.Key}] of group [{experiment.GroupId}].";
-                Logger.Log(LogLevel.INFO, message);
+                Logger.Log(LogLevel.INFO, reasons.AddInfo(message));
             }
 
             // Bucket user if not in whitelist and in group (if any).
             string variationId = FindBucket(bucketingId, userId, experiment.Id, experiment.TrafficAllocation);
             if (string.IsNullOrEmpty(variationId))
             {
-                Logger.Log(LogLevel.INFO, $"User [{userId}] is in no variation.");
+                Logger.Log(LogLevel.INFO, reasons.AddInfo($"User [{userId}] is in no variation."));
                 return new Variation();
 
             }
@@ -149,7 +151,7 @@ public virtual Variation Bucket(ProjectConfig config, Experiment experiment, str
             // success!
             variation = config.GetVariationFromId(experiment.Key, variationId);
             message = $"User [{userId}] is in variation [{variation.Key}] of experiment [{experiment.Key}].";
-            Logger.Log(LogLevel.INFO, message);
+            Logger.Log(LogLevel.INFO, reasons.AddInfo(message));
             return variation;
         }
     }
diff --git a/OptimizelySDK/Bucketing/DecisionService.cs b/OptimizelySDK/Bucketing/DecisionService.cs
index 3959dbbd..3973b13e 100644
--- a/OptimizelySDK/Bucketing/DecisionService.cs
+++ b/OptimizelySDK/Bucketing/DecisionService.cs
@@ -19,6 +19,7 @@
 using OptimizelySDK.Entity;
 using OptimizelySDK.ErrorHandler;
 using OptimizelySDK.Logger;
+using OptimizelySDK.OptimizelyDecisions;
 using OptimizelySDK.Utils;
 
 namespace OptimizelySDK.Bucketing
@@ -83,20 +84,43 @@ public DecisionService(Bucketer bucketer, IErrorHandler errorHandler, UserProfil
         /// <param name = "userId" > The userId of the user.
         /// <param name = "filteredAttributes" > The user's attributes. This should be filtered to just attributes in the Datafile.</param>
         /// <returns>The Variation the user is allocated into.</returns>
-        public virtual Variation GetVariation(Experiment experiment, string userId, ProjectConfig config, UserAttributes filteredAttributes)
+        public virtual Variation GetVariation(Experiment experiment,
+            string userId,
+            ProjectConfig config,
+            UserAttributes filteredAttributes)
+        {
+            return GetVariation(experiment, userId, config, filteredAttributes, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance());
+        }
+
+        /// <summary>
+        /// Get a Variation of an Experiment for a user to be allocated into.
+        /// </summary>
+        /// <param name = "experiment" > The Experiment the user will be bucketed into.</param>
+        /// <param name = "userId" > The userId of the user.
+        /// <param name = "filteredAttributes" > The user's attributes. This should be filtered to just attributes in the Datafile.</param>
+        /// <returns>The Variation the user is allocated into.</returns>
+        public virtual Variation GetVariation(Experiment experiment,
+            string userId,
+            ProjectConfig config,
+            UserAttributes filteredAttributes,
+            List<OptimizelyDecideOption> options,
+            IDecisionReasons reasons)
         {
             if (!ExperimentUtils.IsExperimentActive(experiment, Logger)) return null;
 
             // check if a forced variation is set
-            var forcedVariation = GetForcedVariation(experiment.Key, userId, config);
+            var forcedVariation = GetForcedVariation(experiment.Key, userId, config, reasons);
             if (forcedVariation != null)
                 return forcedVariation;
 
-            var variation = GetWhitelistedVariation(experiment, userId);
+            var variation = GetWhitelistedVariation(experiment, userId, reasons);
 
             if (variation != null) return variation;
+            // fetch the user profile map from the user profile service
+            var ignoreUPS = options.Contains(OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE);
+
             UserProfile userProfile = null;
-            if (UserProfileService != null)
+            if (UserProfileService != null && !ignoreUPS)
             {
                 try
                 {
@@ -104,21 +128,21 @@ public virtual Variation GetVariation(Experiment experiment, string userId, Proj
                     if (userProfileMap != null && UserProfileUtil.IsValidUserProfileMap(userProfileMap))
                     {
                         userProfile = UserProfileUtil.ConvertMapToUserProfile(userProfileMap);
-                        variation = GetStoredVariation(experiment, userProfile, config);
+                        variation = GetStoredVariation(experiment, userProfile, config, reasons);
                         if (variation != null) return variation;
                     }
                     else if (userProfileMap == null)
                     {
-                        Logger.Log(LogLevel.INFO, "We were unable to get a user profile map from the UserProfileService.");
+                        Logger.Log(LogLevel.INFO, reasons.AddInfo("We were unable to get a user profile map from the UserProfileService."));
                     }
                     else
                     {
-                        Logger.Log(LogLevel.ERROR, "The UserProfileService returned an invalid map.");
+                        Logger.Log(LogLevel.ERROR, reasons.AddInfo("The UserProfileService returned an invalid map."));
                     }
                 }
                 catch (Exception exception)
                 {
-                    Logger.Log(LogLevel.ERROR, exception.Message);
+                    Logger.Log(LogLevel.ERROR, reasons.AddInfo(exception.Message));
                     ErrorHandler.HandleError(new Exceptions.OptimizelyRuntimeException(exception.Message));
                 }
             }
@@ -126,16 +150,16 @@ public virtual Variation GetVariation(Experiment experiment, string userId, Proj
             if (ExperimentUtils.DoesUserMeetAudienceConditions(config, experiment, filteredAttributes, LOGGING_KEY_TYPE_EXPERIMENT, experiment.Key, Logger))
             {
                 // Get Bucketing ID from user attributes.
-                string bucketingId = GetBucketingId(userId, filteredAttributes);
+                string bucketingId = GetBucketingId(userId, filteredAttributes, reasons);
 
-                variation = Bucketer.Bucket(config, experiment, bucketingId, userId);
+                variation = Bucketer.Bucket(config, experiment, bucketingId, userId, reasons);
 
                 if (variation != null && variation.Key != null)
                 {
                     if (UserProfileService != null)
                     {
                         var bucketerUserProfile = userProfile ?? new UserProfile(userId, new Dictionary<string, Decision>());
-                        SaveVariation(experiment, variation, bucketerUserProfile);
+                        SaveVariation(experiment, variation, bucketerUserProfile, reasons);
 
                     }
                     else
@@ -144,7 +168,7 @@ public virtual Variation GetVariation(Experiment experiment, string userId, Proj
 
                 return variation;
             }
-            Logger.Log(LogLevel.INFO, $"User \"{userId}\" does not meet conditions to be in experiment \"{experiment.Key}\".");
+            Logger.Log(LogLevel.INFO, reasons.AddInfo($"User \"{userId}\" does not meet conditions to be in experiment \"{experiment.Key}\"."));
 
             return null;
         }
@@ -157,6 +181,18 @@ public virtual Variation GetVariation(Experiment experiment, string userId, Proj
         /// <param name="config">Project Config</param>
         /// <returns>Variation entity which the given user and experiment should be forced into.</returns>
         public Variation GetForcedVariation(string experimentKey, string userId, ProjectConfig config)
+        {
+            return GetForcedVariation(experimentKey, userId, config, DefaultDecisionReasons.NewInstance());
+        }
+
+        /// <summary>
+        /// Gets the forced variation for the given user and experiment.  
+        /// </summary>
+        /// <param name="experimentKey">The experiment key</param>
+        /// <param name="userId">The user ID</param>
+        /// <param name="config">Project Config</param>
+        /// <returns>Variation entity which the given user and experiment should be forced into.</returns>
+        public Variation GetForcedVariation(string experimentKey, string userId, ProjectConfig config, IDecisionReasons reasons)
         {
             if (ForcedVariationMap.ContainsKey(userId) == false)
             {
@@ -191,8 +227,7 @@ public Variation GetForcedVariation(string experimentKey, string userId, Project
             // this case is logged in getVariationFromKey   
             if (string.IsNullOrEmpty(variationKey))
                 return null;
-
-            Logger.Log(LogLevel.DEBUG, $@"Variation ""{variationKey}"" is mapped to experiment ""{experimentKey}"" and user ""{userId}"" in the forced variation map");
+            Logger.Log(LogLevel.DEBUG, reasons.AddInfo($@"Variation ""{variationKey}"" is mapped to experiment ""{experimentKey}"" and user ""{userId}"" in the forced variation map"));
 
             Variation variation = config.GetVariationFromKey(experimentKey, variationKey);
 
@@ -248,8 +283,6 @@ public bool SetForcedVariation(string experimentKey, string userId, string varia
             Logger.Log(LogLevel.DEBUG, $@"Set variation ""{variationId}"" for experiment ""{experimentId}"" and user ""{userId}"" in the forced variation map.");
             return true;
         }
-
-
         /// <summary>
         /// Get the variation the user has been whitelisted into.
         /// </summary>
@@ -258,6 +291,19 @@ public bool SetForcedVariation(string experimentKey, string userId, string varia
         /// <returns>if the user is not whitelisted into any variation {@link Variation}
         /// the user is bucketed into if the user has a specified whitelisted variation.</returns>
         public Variation GetWhitelistedVariation(Experiment experiment, string userId)
+        {
+            return GetWhitelistedVariation(experiment, userId, DefaultDecisionReasons.NewInstance());
+        }
+
+        /// <summary>
+        /// Get the variation the user has been whitelisted into.
+        /// </summary>
+        /// <param name = "experiment" >in which user is to be bucketed.</param>
+        /// <param name = "userId" > User Identifier</param>
+        /// <param name = "reasons" > Decision log messages.</param>
+        /// <returns>if the user is not whitelisted into any variation {@link Variation}
+        /// the user is bucketed into if the user has a specified whitelisted variation.</returns>
+        public Variation GetWhitelistedVariation(Experiment experiment, string userId, IDecisionReasons reasons)
         {
             //if a user has a forced variation mapping, return the respective variation
             Dictionary<string, string> userIdToVariationKeyMap = experiment.UserIdToKeyVariations;
@@ -271,9 +317,9 @@ public Variation GetWhitelistedVariation(Experiment experiment, string userId)
                 : null;
 
             if (forcedVariation != null)
-                Logger.Log(LogLevel.INFO, $"User \"{userId}\" is forced in variation \"{forcedVariationKey}\".");
+                Logger.Log(LogLevel.INFO, reasons.AddInfo($"User \"{userId}\" is forced in variation \"{forcedVariationKey}\"."));
             else
-                Logger.Log(LogLevel.ERROR, $"Variation \"{forcedVariationKey}\" is not in the datafile. Not activating user \"{userId}\".");
+                Logger.Log(LogLevel.ERROR, reasons.AddInfo($"Variation \"{forcedVariationKey}\" is not in the datafile. Not activating user \"{userId}\"."));
 
             return forcedVariation;
         }
@@ -284,7 +330,7 @@ public Variation GetWhitelistedVariation(Experiment experiment, string userId)
         /// <param name = "experiment" > which the user was bucketed</param>
         /// <param name = "userProfile" > User profile of the user</param>
         /// <returns>The user was previously bucketed into.</returns>
-        public Variation GetStoredVariation(Experiment experiment, UserProfile userProfile, ProjectConfig config)
+        public Variation GetStoredVariation(Experiment experiment, UserProfile userProfile, ProjectConfig config, IDecisionReasons reasons)
         {
             // ---------- Check User Profile for Sticky Bucketing ----------
             // If a user profile instance is present then check it for a saved variation
@@ -296,7 +342,7 @@ public Variation GetStoredVariation(Experiment experiment, UserProfile userProfi
 
             if (decision == null)
             {
-                Logger.Log(LogLevel.INFO, $"No previously activated variation of experiment \"{experimentKey}\" for user \"{userProfile.UserId}\" found in user profile.");
+                Logger.Log(LogLevel.INFO, reasons.AddInfo($"No previously activated variation of experiment \"{experimentKey}\" for user \"{userProfile.UserId}\" found in user profile."));
                 return null;
             }
 
@@ -310,11 +356,11 @@ public Variation GetStoredVariation(Experiment experiment, UserProfile userProfi
 
                 if (savedVariation == null)
                 {
-                    Logger.Log(LogLevel.INFO, $"User \"{userProfile.UserId}\" was previously bucketed into variation with ID \"{variationId}\" for experiment \"{experimentId}\", but no matching variation was found for that user. We will re-bucket the user.");
+                    Logger.Log(LogLevel.INFO, reasons.AddInfo($"User \"{userProfile.UserId}\" was previously bucketed into variation with ID \"{variationId}\" for experiment \"{experimentId}\", but no matching variation was found for that user. We will re-bucket the user."));
                     return null;
                 }
 
-                Logger.Log(LogLevel.INFO, $"Returning previously activated variation \"{savedVariation.Key}\" of experiment \"{experimentKey}\" for user \"{userProfile.UserId}\" from user profile.");
+                Logger.Log(LogLevel.INFO, reasons.AddInfo($"Returning previously activated variation \"{savedVariation.Key}\" of experiment \"{experimentKey}\" for user \"{userProfile.UserId}\" from user profile."));
                 return savedVariation;
             }
             catch (Exception)
@@ -322,7 +368,6 @@ public Variation GetStoredVariation(Experiment experiment, UserProfile userProfi
                 return null;
             }
         }
-
         /// <summary>
         /// Save a { @link Variation } of an { @link Experiment } for a user in the {@link UserProfileService}.
         /// </summary>
@@ -330,6 +375,17 @@ public Variation GetStoredVariation(Experiment experiment, UserProfile userProfi
         /// <param name = "variation" > The Variation to save.</param>
         /// <param name = "userProfile" > instance of the user information.</param>
         public void SaveVariation(Experiment experiment, Variation variation, UserProfile userProfile)
+        {
+            SaveVariation(experiment, variation, userProfile, DefaultDecisionReasons.NewInstance());
+        }
+
+        /// <summary>
+        /// Save a { @link Variation } of an { @link Experiment } for a user in the {@link UserProfileService}.
+        /// </summary>
+        /// <param name = "experiment" > The experiment the user was buck</param>
+        /// <param name = "variation" > The Variation to save.</param>
+        /// <param name = "userProfile" > instance of the user information.</param>
+        public void SaveVariation(Experiment experiment, Variation variation, UserProfile userProfile, IDecisionReasons reasons)
         {
             //only save if the user has implemented a user profile service
             if (UserProfileService == null)
@@ -351,15 +407,15 @@ public void SaveVariation(Experiment experiment, Variation variation, UserProfil
             try
             {
                 UserProfileService.Save(userProfile.ToMap());
-                Logger.Log(LogLevel.INFO, $"Saved variation \"{variation.Id}\" of experiment \"{experiment.Id}\" for user \"{userProfile.UserId}\".");
+                Logger.Log(LogLevel.INFO, reasons.AddInfo($"Saved variation \"{variation.Id}\" of experiment \"{experiment.Id}\" for user \"{userProfile.UserId}\"."));
             }
             catch (Exception exception)
             {
-                Logger.Log(LogLevel.ERROR, $"Failed to save variation \"{variation.Id}\" of experiment \"{experiment.Id}\" for user \"{userProfile.UserId}\".");
+                Logger.Log(LogLevel.ERROR, reasons.AddInfo($"Failed to save variation \"{variation.Id}\" of experiment \"{experiment.Id}\" for user \"{userProfile.UserId}\"."));
                 ErrorHandler.HandleError(new Exceptions.OptimizelyRuntimeException(exception.Message));
             }
         }
-
+       
         /// <summary>
         /// Try to bucket the user into a rollout rule.
         /// Evaluate the user for rules in priority order by seeing if the user satisfies the audience.
@@ -368,9 +424,14 @@ public void SaveVariation(Experiment experiment, Variation variation, UserProfil
         /// <param name = "featureFlag" >The feature flag the user wants to access.</param>
         /// <param name = "userId" >User Identifier</param>
         /// <param name = "filteredAttributes" >The user's attributes. This should be filtered to just attributes in the Datafile.</param>
+        /// <param name = "reasons" >Decision log messages.</param>
         /// <returns>null if the user is not bucketed into the rollout or if the feature flag was not attached to a rollout.
         /// otherwise the FeatureDecision entity</returns>
-        public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag featureFlag, string userId, UserAttributes filteredAttributes, ProjectConfig config)
+        public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag featureFlag,
+            string userId,
+            UserAttributes filteredAttributes,
+            ProjectConfig config,
+            IDecisionReasons reasons)
         {
             if (featureFlag == null)
             {
@@ -380,7 +441,7 @@ public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag feature
 
             if (string.IsNullOrEmpty(featureFlag.RolloutId))
             {
-                Logger.Log(LogLevel.INFO, $"The feature flag \"{featureFlag.Key}\" is not used in a rollout.");
+                Logger.Log(LogLevel.INFO, reasons.AddInfo($"The feature flag \"{featureFlag.Key}\" is not used in a rollout."));
                 return null;
             }
 
@@ -388,7 +449,7 @@ public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag feature
 
             if (string.IsNullOrEmpty(rollout.Id))
             {
-                Logger.Log(LogLevel.ERROR, $"The rollout with id \"{featureFlag.RolloutId}\" is not found in the datafile for feature flag \"{featureFlag.Key}\"");
+                Logger.Log(LogLevel.ERROR, reasons.AddInfo($"The rollout with id \"{featureFlag.RolloutId}\" is not found in the datafile for feature flag \"{featureFlag.Key}\""));
                 return null;
             }
 
@@ -400,16 +461,16 @@ public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag feature
             var rolloutRulesLength = rollout.Experiments.Count;
 
             // Get Bucketing ID from user attributes.
-            string bucketingId = GetBucketingId(userId, filteredAttributes);
+            string bucketingId = GetBucketingId(userId, filteredAttributes, reasons);
 
             // For all rules before the everyone else rule
             for (int i = 0; i < rolloutRulesLength - 1; i++)
             {
                 string loggingKey = (i + 1).ToString(); 
                 var rolloutRule = rollout.Experiments[i];
-                if (ExperimentUtils.DoesUserMeetAudienceConditions(config, rolloutRule, filteredAttributes, LOGGING_KEY_TYPE_RULE, loggingKey, Logger))
+                if (ExperimentUtils.DoesUserMeetAudienceConditions(config, rolloutRule, filteredAttributes, LOGGING_KEY_TYPE_RULE, loggingKey, reasons, Logger))
                 {
-                    variation = Bucketer.Bucket(config, rolloutRule, bucketingId, userId);
+                    variation = Bucketer.Bucket(config, rolloutRule, bucketingId, userId, reasons);
                     if (variation == null || string.IsNullOrEmpty(variation.Id))
                         break;
 
@@ -424,9 +485,9 @@ public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag feature
 
             // Get the last rule which is everyone else rule.
             var everyoneElseRolloutRule = rollout.Experiments[rolloutRulesLength - 1];
-            if (ExperimentUtils.DoesUserMeetAudienceConditions(config, everyoneElseRolloutRule, filteredAttributes, LOGGING_KEY_TYPE_RULE, "Everyone Else", Logger))
+            if (ExperimentUtils.DoesUserMeetAudienceConditions(config, everyoneElseRolloutRule, filteredAttributes, LOGGING_KEY_TYPE_RULE, "Everyone Else", reasons, Logger))
             {
-                variation = Bucketer.Bucket(config, everyoneElseRolloutRule, bucketingId, userId);
+                variation = Bucketer.Bucket(config, everyoneElseRolloutRule, bucketingId, userId, reasons);
                 if (variation != null && !string.IsNullOrEmpty(variation.Id))
                 {
                     Logger.Log(LogLevel.DEBUG, $"User \"{userId}\" meets conditions for targeting rule \"Everyone Else\".");
@@ -450,7 +511,12 @@ public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag feature
         /// <param name = "filteredAttributes" >The user's attributes. This should be filtered to just attributes in the Datafile.</param>
         /// <returns>null if the user is not bucketed into the rollout or if the feature flag was not attached to a rollout.
         /// Otherwise the FeatureDecision entity</returns>
-        public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag featureFlag, string userId, UserAttributes filteredAttributes, ProjectConfig config)
+        public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag featureFlag,
+            string userId,
+            UserAttributes filteredAttributes,
+            ProjectConfig config,
+            List<OptimizelyDecideOption> options,
+            IDecisionReasons reasons)
         {
             if (featureFlag == null)
             {
@@ -460,7 +526,7 @@ public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag feat
 
             if (featureFlag.ExperimentIds == null || featureFlag.ExperimentIds.Count == 0)
             {
-                Logger.Log(LogLevel.INFO, $"The feature flag \"{featureFlag.Key}\" is not used in any experiments.");
+                Logger.Log(LogLevel.INFO, reasons.AddInfo($"The feature flag \"{featureFlag.Key}\" is not used in any experiments."));
                 return null;
             }
 
@@ -471,16 +537,16 @@ public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag feat
                 if (string.IsNullOrEmpty(experiment.Key))
                     continue;
 
-                var variation = GetVariation(experiment, userId, config, filteredAttributes);
+                var variation = GetVariation(experiment, userId, config, filteredAttributes, options, reasons);
 
                 if (variation != null && !string.IsNullOrEmpty(variation.Id))
                 {
-                    Logger.Log(LogLevel.INFO, $"The user \"{userId}\" is bucketed into experiment \"{experiment.Key}\" of feature \"{featureFlag.Key}\".");
+                    Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is bucketed into experiment \"{experiment.Key}\" of feature \"{featureFlag.Key}\"."));
                     return new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST);
                 }
             }
 
-            Logger.Log(LogLevel.INFO, $"The user \"{userId}\" is not bucketed into any of the experiments on the feature \"{featureFlag.Key}\".");
+            Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is not bucketed into any of the experiments on the feature \"{featureFlag.Key}\"."));
             return null;
         }
 
@@ -493,23 +559,44 @@ public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag feat
         /// <returns>null if the user is not bucketed into any variation or the FeatureDecision entity if the user is 
         /// successfully bucketed.</returns>
         public virtual FeatureDecision GetVariationForFeature(FeatureFlag featureFlag, string userId, ProjectConfig config, UserAttributes filteredAttributes)
+        {
+            return GetVariationForFeature(featureFlag, userId, config, filteredAttributes, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance());
+        }
+
+        /// <summary>
+        /// Get the variation the user is bucketed into for the FeatureFlag
+        /// </summary>
+        /// <param name = "featureFlag" >The feature flag the user wants to access.</param>
+        /// <param name = "userId" >User Identifier</param>
+        /// <param name = "filteredAttributes" >The user's attributes. This should be filtered to just attributes in the Datafile.</param>
+        /// <param name = "filteredAttributes" >The user's attributes. This should be filtered to just attributes in the Datafile.</param>
+        /// <param name = "options" >An array of decision options.</param>
+        /// <param name = "reasons" >Decision log messages.</param>
+        /// <returns>null if the user is not bucketed into any variation or the FeatureDecision entity if the user is 
+        /// successfully bucketed.</returns>
+        public virtual FeatureDecision GetVariationForFeature(FeatureFlag featureFlag,
+            string userId,
+            ProjectConfig config,
+            UserAttributes filteredAttributes,
+            List<OptimizelyDecideOption> options, 
+            IDecisionReasons reasons)
         {
             // Check if the feature flag has an experiment and the user is bucketed into that experiment.
-            var decision = GetVariationForFeatureExperiment(featureFlag, userId, filteredAttributes, config);
+            var decision = GetVariationForFeatureExperiment(featureFlag, userId, filteredAttributes, config, options, reasons);
 
             if (decision != null)
                 return decision;
 
             // Check if the feature flag has rollout and the the user is bucketed into one of its rules.
-            decision = GetVariationForFeatureRollout(featureFlag, userId, filteredAttributes, config);
+            decision = GetVariationForFeatureRollout(featureFlag, userId, filteredAttributes, config, reasons);
 
             if (decision != null)
             {
-                Logger.Log(LogLevel.INFO, $"The user \"{userId}\" is bucketed into a rollout for feature flag \"{featureFlag.Key}\".");
+                Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is bucketed into a rollout for feature flag \"{featureFlag.Key}\"."));
                 return decision;
             }
 
-            Logger.Log(LogLevel.INFO, $"The user \"{userId}\" is not bucketed into a rollout for feature flag \"{featureFlag.Key}\".");
+            Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is not bucketed into a rollout for feature flag \"{featureFlag.Key}\"."));
             return new FeatureDecision(null, null, FeatureDecision.DECISION_SOURCE_ROLLOUT);
         }
 
@@ -519,7 +606,7 @@ public virtual FeatureDecision GetVariationForFeature(FeatureFlag featureFlag, s
         /// <param name = "userId" >User Identifier</param>
         /// <param name = "filteredAttributes" >The user's attributes.</param>
         /// <returns>Bucketing Id if it is a string type in attributes, user Id otherwise.</returns>
-        private string GetBucketingId(string userId, UserAttributes filteredAttributes)
+        private string GetBucketingId(string userId, UserAttributes filteredAttributes, IDecisionReasons reasons)
         {
             string bucketingId = userId;
 
@@ -533,7 +620,7 @@ private string GetBucketingId(string userId, UserAttributes filteredAttributes)
                 }
                 else
                 {
-                    Logger.Log(LogLevel.WARN, "BucketingID attribute is not a string. Defaulted to userId");
+                    Logger.Log(LogLevel.WARN, reasons.AddInfo("BucketingID attribute is not a string. Defaulted to userId"));
                 }
             }
 
diff --git a/OptimizelySDK/Optimizely.cs b/OptimizelySDK/Optimizely.cs
index b30d00ac..49901a1c 100644
--- a/OptimizelySDK/Optimizely.cs
+++ b/OptimizelySDK/Optimizely.cs
@@ -476,45 +476,45 @@ public virtual bool IsFeatureEnabled(string featureKey, string userId, UserAttri
                 return false;
 
             bool featureEnabled = false;
-            var sourceInfo = new Dictionary<string, string>();
-            var decision = DecisionService.GetVariationForFeature(featureFlag, userId, config, userAttributes);
-            var variation = decision.Variation;
-            var decisionSource = decision?.Source ?? FeatureDecision.DECISION_SOURCE_ROLLOUT;
-
-            SendImpressionEvent(decision.Experiment, variation, userId, userAttributes, config, featureKey, decisionSource);
+                var sourceInfo = new Dictionary<string, string>();
+                var decision = DecisionService.GetVariationForFeature(featureFlag, userId, config, userAttributes);
+                var variation = decision.Variation;
+                var decisionSource = decision?.Source ?? FeatureDecision.DECISION_SOURCE_ROLLOUT;
 
-            if (variation != null)
-            {
-                featureEnabled = variation.FeatureEnabled.GetValueOrDefault();
+                SendImpressionEvent(decision.Experiment, variation, userId, userAttributes, config, featureKey, decisionSource);
 
-                // This information is only necessary for feature tests.
-                // For rollouts experiments and variations are an implementation detail only.
-                if (decision.Source == FeatureDecision.DECISION_SOURCE_FEATURE_TEST)
-                {
-                    sourceInfo["experimentKey"] = decision.Experiment.Key;
-                    sourceInfo["variationKey"] = variation.Key;
-                }
-                else
+                if (variation != null)
                 {
-                    Logger.Log(LogLevel.INFO, $@"The user ""{userId}"" is not being experimented on feature ""{featureKey}"".");
-                }
-            }
+                    featureEnabled = variation.FeatureEnabled.GetValueOrDefault();
 
-            if (featureEnabled == true)
-                Logger.Log(LogLevel.INFO, $@"Feature flag ""{featureKey}"" is enabled for user ""{userId}"".");
-            else
-                Logger.Log(LogLevel.INFO, $@"Feature flag ""{featureKey}"" is not enabled for user ""{userId}"".");
+                    // This information is only necessary for feature tests.
+                    // For rollouts experiments and variations are an implementation detail only.
+                    if (decision.Source == FeatureDecision.DECISION_SOURCE_FEATURE_TEST)
+                    {
+                        sourceInfo["experimentKey"] = decision.Experiment.Key;
+                        sourceInfo["variationKey"] = variation.Key;
+                    }
+                    else
+                    {
+                        Logger.Log(LogLevel.INFO, $@"The user ""{userId}"" is not being experimented on feature ""{featureKey}"".");
+                    }
+                }
 
-            var decisionInfo = new Dictionary<string, object>
-            {
-                { "featureKey", featureKey },
-                { "featureEnabled", featureEnabled },
-                { "source", decision.Source },
-                { "sourceInfo", sourceInfo },
-            };
+                if (featureEnabled == true)
+                    Logger.Log(LogLevel.INFO, $@"Feature flag ""{featureKey}"" is enabled for user ""{userId}"".");
+                else
+                    Logger.Log(LogLevel.INFO, $@"Feature flag ""{featureKey}"" is not enabled for user ""{userId}"".");
 
-            NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision, DecisionNotificationTypes.FEATURE, userId,
-               userAttributes ?? new UserAttributes(), decisionInfo);
+                var decisionInfo = new Dictionary<string, object>
+                {
+                    { "featureKey", featureKey },
+                    { "featureEnabled", featureEnabled },
+                    { "source", decision.Source },
+                    { "sourceInfo", sourceInfo },
+                };
+
+                NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision, DecisionNotificationTypes.FEATURE, userId,
+                   userAttributes ?? new UserAttributes(), decisionInfo);
             return featureEnabled;
         }
 
@@ -712,6 +712,139 @@ public OptimizelyUserContext CreateUserContext(string userId,
             return new OptimizelyUserContext(this, userId, userAttributes, ErrorHandler, Logger);
         }
 
+        public OptimizelyDecision Decide(OptimizelyUserContext user,
+                              string key,
+                              List<OptimizelyDecideOption> options)
+        {
+
+            var config = ProjectConfigManager?.GetConfig();
+            if (config == null)
+            {
+                return OptimizelyDecision.NewErrorDecision(key, user, DecisionMessage.SDK_NOT_READY, ErrorHandler, Logger);
+            }
+            var userId = user.UserId;
+            var inputValues = new Dictionary<string, string>
+            {
+                { USER_ID, userId },
+            };
+
+            if (!ValidateStringInputs(inputValues))
+                return null;
+
+            var flag = config.GetFeatureFlagFromKey(key);
+            if (flag == null)
+            {
+                return OptimizelyDecision.NewErrorDecision(key,
+                    user,
+                    DecisionMessage.Reason(DecisionMessage.FLAG_KEY_INVALID, key),
+                    ErrorHandler, Logger);
+            }
+
+            var userAttributes = user.UserAttributes;
+            var decisionEventDispatched = false;
+            var allOptions = GetAllOptions(options);
+            var decisionReasons = DefaultDecisionReasons.NewInstance(allOptions);
+
+            var flagDecision = DecisionService.GetVariationForFeature(
+                flag,
+                userId,
+                config,
+                userAttributes,
+                allOptions,
+                decisionReasons);
+
+            var featureEnabled = false;
+
+            var variation = flagDecision.Variation;
+
+            if (variation != null)
+            {
+                featureEnabled = variation.FeatureEnabled.GetValueOrDefault();
+            }
+            
+            if (featureEnabled)
+            {
+                Logger.Log(LogLevel.INFO, "Feature \"" + key + "\" is enabled for user \"" + userId + "\"");
+            }
+            else
+            {
+                Logger.Log(LogLevel.INFO, "Feature \"" + key + "\" is not enabled for user \"" + userId + "\"");
+            }
+            var variableMap = new Dictionary<string, object>();
+            if (flag?.Variables != null && !allOptions.Contains(OptimizelyDecideOption.EXCLUDE_VARIABLES))
+            {
+
+                foreach (var featureVariable in flag?.Variables)
+                {
+                    string variableValue = featureVariable.DefaultValue;
+                    if (featureEnabled)
+                    {
+                        var featureVariableUsageInstance = variation.GetFeatureVariableUsageFromId(featureVariable.Id);
+                        if (featureVariableUsageInstance != null)
+                        {
+                            variableValue = featureVariableUsageInstance.Value;
+                        }
+                    }
+
+                    var typeCastedValue = GetTypeCastedVariableValue(variableValue, featureVariable.Type);
+
+                    if (typeCastedValue is OptimizelyJSON)
+                        typeCastedValue = ((OptimizelyJSON)typeCastedValue).ToDictionary();
+
+                    variableMap.Add(featureVariable.Key, typeCastedValue);
+                }
+            }
+            
+            var optimizelyJSON = new OptimizelyJSON(variableMap, ErrorHandler, Logger);
+
+            var decisionSource = flagDecision?.Source ?? FeatureDecision.DECISION_SOURCE_ROLLOUT;
+            if (!allOptions.Contains(OptimizelyDecideOption.DISABLE_DECISION_EVENT))
+            {
+
+                SendImpressionEvent(flagDecision.Experiment, variation, userId, userAttributes, config, key, decisionSource);
+                decisionEventDispatched = true;
+            }
+            List<string> reasonsToReport = decisionReasons.ToReport();
+            string variationKey = flagDecision.Variation?.Key;
+
+            // TODO: add ruleKey values when available later. use a copy of experimentKey until then.
+            string ruleKey = flagDecision.Experiment?.Key;
+
+            var decisionInfo = new Dictionary<string, object>
+            {
+                { "flagKey", key },
+                { "enabled", featureEnabled },
+                { "variables", variableMap },
+                { "variationKey", variationKey },
+                { "ruleKey", ruleKey },
+                { "reasons", decisionReasons },
+                { "decisionEventDispatched", decisionEventDispatched },
+                { "featureEnabled", featureEnabled },
+            };
+
+            NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision, DecisionNotificationTypes.FEATURE, userId,
+               userAttributes ?? new UserAttributes(), decisionInfo);
+
+            return new OptimizelyDecision(
+                variationKey,
+                featureEnabled,
+                optimizelyJSON,
+                ruleKey,
+                key,
+                user,
+                reasonsToReport);
+        }
+
+        private List<OptimizelyDecideOption> GetAllOptions(List<OptimizelyDecideOption> options)
+        {
+            var copiedOptions = new List<OptimizelyDecideOption>(DefaultDecideOptions);
+            if (options != null)
+            {
+                copiedOptions.AddRange(options);
+            }
+            return copiedOptions;
+        }
+
         /// <summary>
         /// Sends impression event.
         /// </summary>
diff --git a/OptimizelySDK/OptimizelyUserContext.cs b/OptimizelySDK/OptimizelyUserContext.cs
index e2621f20..07834546 100644
--- a/OptimizelySDK/OptimizelyUserContext.cs
+++ b/OptimizelySDK/OptimizelyUserContext.cs
@@ -46,7 +46,7 @@ public OptimizelyUserContext(Optimizely optimizely, string userId, UserAttribute
         /// <param name="value">value An attribute value</param>
         public void SetAttribute(string key, object value)
         {
-            UserAttributes.Add(key, value);
+            UserAttributes[key] = value;
         }
 
         /// <summary>
@@ -74,7 +74,7 @@ public OptimizelyDecision Decide(string key)
         public OptimizelyDecision Decide(string key,
             List<OptimizelyDecideOption> options)
         {
-            return null;
+            return Optimizely.Decide(this, key, options);
         }
  
         /// <summary>
diff --git a/OptimizelySDK/Utils/ExperimentUtils.cs b/OptimizelySDK/Utils/ExperimentUtils.cs
index 470b014c..b6023480 100644
--- a/OptimizelySDK/Utils/ExperimentUtils.cs
+++ b/OptimizelySDK/Utils/ExperimentUtils.cs
@@ -17,6 +17,7 @@
 using OptimizelySDK.AudienceConditions;
 using OptimizelySDK.Entity;
 using OptimizelySDK.Logger;
+using OptimizelySDK.OptimizelyDecisions;
 
 namespace OptimizelySDK.Utils
 {
@@ -35,6 +36,24 @@ public static bool IsExperimentActive(Experiment experiment, ILogger logger)
             return true;
         }
 
+        /// <summary>
+        /// Check if the user meets audience conditions to be in experiment or not
+        /// </summary>
+        /// <param name="config">ProjectConfig Configuration for the project</param>
+        /// <param name="experiment">Experiment Entity representing the experiment</param>
+        /// <param name="userAttributes">Attributes of the user. Defaults to empty attributes array if not provided</param>
+        /// <param name="loggingKeyType">It can be either experiment or rule.</param>
+        /// <param name="loggingKey">In case loggingKeyType is experiment it will be experiment key or else it will be rule number.</param>
+        /// <returns>true if the user meets audience conditions to be in experiment, false otherwise.</returns>
+        public static bool DoesUserMeetAudienceConditions(ProjectConfig config,
+            Experiment experiment,
+            UserAttributes userAttributes,
+            string loggingKeyType,
+            string loggingKey,
+            ILogger logger)
+        {
+            return DoesUserMeetAudienceConditions(config, experiment, userAttributes, loggingKeyType, loggingKey, DefaultDecisionReasons.NewInstance(), logger);
+        }
 
         /// <summary>
         /// Check if the user meets audience conditions to be in experiment or not
@@ -44,12 +63,14 @@ public static bool IsExperimentActive(Experiment experiment, ILogger logger)
         /// <param name="userAttributes">Attributes of the user. Defaults to empty attributes array if not provided</param>
         /// <param name="loggingKeyType">It can be either experiment or rule.</param>
         /// <param name="loggingKey">In case loggingKeyType is experiment it will be experiment key or else it will be rule number.</param>
+        /// <param name="reasons">Decision log messages.</param>
         /// <returns>true if the user meets audience conditions to be in experiment, false otherwise.</returns>
         public static bool DoesUserMeetAudienceConditions(ProjectConfig config,
             Experiment experiment,
             UserAttributes userAttributes,
             string loggingKeyType,
             string loggingKey,
+            IDecisionReasons reasons,
             ILogger logger)
         {
             if (userAttributes == null)
@@ -73,7 +94,7 @@ public static bool DoesUserMeetAudienceConditions(ProjectConfig config,
 
             var result = expConditions.Evaluate(config, userAttributes, logger).GetValueOrDefault();
             var resultText = result.ToString().ToUpper();
-            logger.Log(LogLevel.INFO, $@"Audiences for {loggingKeyType} ""{loggingKey}"" collectively evaluated to {resultText}");
+            logger.Log(LogLevel.INFO, reasons.AddInfo($@"Audiences for {loggingKeyType} ""{loggingKey}"" collectively evaluated to {resultText}"));
             return result;
         }
     }

From 9e36946f438bda4d0e59b7f0d4bc0956c45125b3 Mon Sep 17 00:00:00 2001
From: muhammadnoman <muhammadnoman@folio3.com>
Date: Wed, 11 Nov 2020 23:02:21 +0500
Subject: [PATCH 08/19] Revert "Added UserContext add reason logs"

This reverts commit 6c9ac4d72c9a7a0cdf9539b70caed58145bc76a5.
---
 OptimizelySDK.Tests/BucketerTest.cs           |  23 +-
 OptimizelySDK.Tests/DecisionServiceTest.cs    |  93 ++++----
 .../OptimizelyUserContextTest.cs              | 201 ------------------
 OptimizelySDK/Bucketing/Bucketer.cs           |  14 +-
 OptimizelySDK/Bucketing/DecisionService.cs    | 177 ++++-----------
 OptimizelySDK/Optimizely.cs                   | 199 +++--------------
 OptimizelySDK/OptimizelyUserContext.cs        |   4 +-
 OptimizelySDK/Utils/ExperimentUtils.cs        |  23 +-
 8 files changed, 144 insertions(+), 590 deletions(-)
 delete mode 100644 OptimizelySDK.Tests/OptimizelyUserContextTest.cs

diff --git a/OptimizelySDK.Tests/BucketerTest.cs b/OptimizelySDK.Tests/BucketerTest.cs
index 78b0feec..aeeeb96c 100644
--- a/OptimizelySDK.Tests/BucketerTest.cs
+++ b/OptimizelySDK.Tests/BucketerTest.cs
@@ -19,7 +19,6 @@
 using OptimizelySDK.Config;
 using OptimizelySDK.Entity;
 using OptimizelySDK.Logger;
-using OptimizelySDK.OptimizelyDecisions;
 
 namespace OptimizelySDK.Tests
 {
@@ -99,7 +98,7 @@ public void TestBucketValidExperimentNotInGroup()
 
             // control
             Assert.AreEqual(new Variation { Id = "7722370027", Key = "control" },
-                bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance()));
+                bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId));
 
             LoggerMock.Verify(l => l.Log(It.IsAny<LogLevel>(), It.IsAny<string>()), Times.Exactly(2));
             LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [3000] to user [testUserId] with bucketing ID [testBucketingIdControl!]."));
@@ -107,7 +106,7 @@ public void TestBucketValidExperimentNotInGroup()
 
             // variation
             Assert.AreEqual(new Variation { Id = "7721010009", Key = "variation" },
-                bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance()));
+                bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId));
 
             LoggerMock.Verify(l => l.Log(It.IsAny<LogLevel>(), It.IsAny<string>()), Times.Exactly(4));
             LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [7000] to user [testUserId] with bucketing ID [testBucketingIdControl!]."));
@@ -115,7 +114,7 @@ public void TestBucketValidExperimentNotInGroup()
 
             // no variation
             Assert.AreEqual(new Variation { },
-                bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance()));
+                bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId));
 
             LoggerMock.Verify(l => l.Log(It.IsAny<LogLevel>(), It.IsAny<string>()), Times.Exactly(6));
             LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [9000] to user [testUserId] with bucketing ID [testBucketingIdControl!]."));
@@ -131,7 +130,7 @@ public void TestBucketValidExperimentInGroup()
             // variation 1
             bucketer.SetBucketValues(new[] { 1000, 4000 });
             Assert.AreEqual(new Variation { Id = "7722260071", Key = "group_exp_1_var_1" },
-                bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance()));
+                bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId));
             LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [1000] to user [testUserId] with bucketing ID [testBucketingIdControl!]."));
             LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [testUserId] is in experiment [group_experiment_1] of group [7722400015]."));
             LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [4000] to user [testUserId] with bucketing ID [testBucketingIdControl!]."));
@@ -140,7 +139,7 @@ public void TestBucketValidExperimentInGroup()
             // variation 2
             bucketer.SetBucketValues(new[] { 1500, 7000 });
             Assert.AreEqual(new Variation { Id = "7722360022", Key = "group_exp_1_var_2" },
-                bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance()));
+                bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId));
             LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [1500] to user [testUserId] with bucketing ID [testBucketingIdControl!]."));
             LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [testUserId] is in experiment [group_experiment_1] of group [7722400015]."));
             LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [7000] to user [testUserId] with bucketing ID [testBucketingIdControl!]."));
@@ -149,7 +148,7 @@ public void TestBucketValidExperimentInGroup()
             // User not in experiment
             bucketer.SetBucketValues(new[] { 5000, 7000 });
             Assert.AreEqual(new Variation { },
-                bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance()));
+                bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId));
             LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [5000] to user [testUserId] with bucketing ID [testBucketingIdControl!]."));
             LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [testUserId] is not in experiment [group_experiment_1] of group [7722400015]."));
 
@@ -162,7 +161,7 @@ public void TestBucketInvalidExperiment()
             var bucketer = new Bucketer(LoggerMock.Object);
 
             Assert.AreEqual(new Variation { },
-                bucketer.Bucket(Config, new Experiment(), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance()));
+                bucketer.Bucket(Config, new Experiment(), TestBucketingIdControl, TestUserId));
 
             LoggerMock.Verify(l => l.Log(It.IsAny<LogLevel>(), It.IsAny<string>()), Times.Never);
         }
@@ -177,7 +176,7 @@ public void TestBucketWithBucketingId()
 
             // make sure that the bucketing ID is used for the variation bucketing and not the user ID
             Assert.AreEqual(expectedVariation, 
-                bucketer.Bucket(Config, experiment, TestBucketingIdControl, TestUserIdBucketsToVariation, DefaultDecisionReasons.NewInstance()));
+                bucketer.Bucket(Config, experiment, TestBucketingIdControl, TestUserIdBucketsToVariation));
         }
 
         // Test for invalid experiment keys, null variation should be returned
@@ -188,7 +187,7 @@ public void TestBucketVariationInvalidExperimentsWithBucketingId()
             var expectedVariation = new Variation();
 
             Assert.AreEqual(expectedVariation, 
-                bucketer.Bucket(Config, Config.GetExperimentFromKey("invalid_experiment"), TestBucketingIdVariation, TestUserId, DefaultDecisionReasons.NewInstance()));
+                bucketer.Bucket(Config, Config.GetExperimentFromKey("invalid_experiment"), TestBucketingIdVariation, TestUserId));
         }
 
         // Make sure that the bucketing ID is used to bucket the user into a group and not the user ID
@@ -201,7 +200,7 @@ public void TestBucketVariationGroupedExperimentsWithBucketingId()
 
             Assert.AreEqual(expectedGroupVariation,
                 bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_2"), 
-                TestBucketingIdGroupExp2Var2, TestUserIdBucketsToNoGroup, DefaultDecisionReasons.NewInstance()));
+                TestBucketingIdGroupExp2Var2, TestUserIdBucketsToNoGroup));
         }
 
         // Make sure that user gets bucketed into the rollout rule.
@@ -214,7 +213,7 @@ public void TestBucketRolloutRule()
             var expectedVariation = Config.GetVariationFromId(rolloutRule.Key, "177773");
 
             Assert.True(TestData.CompareObjects(expectedVariation, 
-                bucketer.Bucket(Config, rolloutRule, "testBucketingId", TestUserId, DefaultDecisionReasons.NewInstance())));
+                bucketer.Bucket(Config, rolloutRule, "testBucketingId", TestUserId)));
         }
     }
 }
\ No newline at end of file
diff --git a/OptimizelySDK.Tests/DecisionServiceTest.cs b/OptimizelySDK.Tests/DecisionServiceTest.cs
index 3c83b3db..0936a857 100644
--- a/OptimizelySDK.Tests/DecisionServiceTest.cs
+++ b/OptimizelySDK.Tests/DecisionServiceTest.cs
@@ -24,7 +24,6 @@
 using OptimizelySDK.Bucketing;
 using OptimizelySDK.Utils;
 using OptimizelySDK.Config;
-using OptimizelySDK.OptimizelyDecisions;
 
 namespace OptimizelySDK.Tests
 {
@@ -85,7 +84,7 @@ public void TestGetVariationForcedVariationPrecedesAudienceEval()
             LoggerMock.Verify(l => l.Log(LogLevel.INFO, string.Format("User \"{0}\" is forced in variation \"vtag5\".", WhitelistedUserId)), Times.Once);
             // no attributes provided for a experiment that has an audience
             Assert.IsTrue(TestData.CompareObjects(actualVariation, expectedVariation));
-            BucketerMock.Verify(_ => _.Bucket(It.IsAny<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>()), Times.Never);
+            BucketerMock.Verify(_ => _.Bucket(It.IsAny<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never);
         }
 
         [Test]
@@ -112,7 +111,7 @@ public void TestGetVariationEvaluatesUserProfileBeforeAudienceTargeting()
             // ensure that a user with a saved user profile, sees the same variation regardless of audience evaluation
             decisionService.GetVariation(experiment, UserProfileId, ProjectConfig, new UserAttributes());
 
-            BucketerMock.Verify(_ => _.Bucket(It.IsAny<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>()), Times.Never);
+            BucketerMock.Verify(_ => _.Bucket(It.IsAny<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never);
         }
 
         [Test]
@@ -124,7 +123,7 @@ public void TestGetForcedVariationReturnsForcedVariation()
             LoggerMock.Verify(l => l.Log(LogLevel.INFO, string.Format("User \"{0}\" is forced in variation \"{1}\".",
                 WhitelistedUserId, WhitelistedVariation.Key)), Times.Once);
 
-            BucketerMock.Verify(_ => _.Bucket(It.IsAny<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>()), Times.Never);
+            BucketerMock.Verify(_ => _.Bucket(It.IsAny<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never);
         }
 
         [Test]
@@ -165,7 +164,7 @@ public void TestGetForcedVariationWithInvalidVariation()
                 string.Format("Variation \"{0}\" is not in the datafile. Not activating user \"{1}\".", invalidVariationKey, userId)),
                 Times.Once);
 
-            BucketerMock.Verify(_ => _.Bucket(It.IsAny<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>()), Times.Never);
+            BucketerMock.Verify(_ => _.Bucket(It.IsAny<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never);
         }
 
         [Test]
@@ -217,7 +216,7 @@ public void TestGetStoredVariationLogsWhenLookupReturnsNull()
             DecisionService decisionService = new DecisionService(bucketer,
                  ErrorHandlerMock.Object, userProfileService, LoggerMock.Object);
 
-            Assert.IsNull(decisionService.GetStoredVariation(experiment, userProfile, ProjectConfig, DefaultDecisionReasons.NewInstance()));
+            Assert.IsNull(decisionService.GetStoredVariation(experiment, userProfile, ProjectConfig));
 
             LoggerMock.Verify(l => l.Log(LogLevel.INFO, string.Format("No previously activated variation of experiment \"{0}\" for user \"{1}\" found in user profile."
                 , experiment.Key, UserProfileId)), Times.Once);
@@ -242,7 +241,7 @@ public void TestGetStoredVariationReturnsNullWhenVariationIsNoLongerInConfig()
 
             DecisionService decisionService = new DecisionService(bucketer, ErrorHandlerMock.Object,
                 UserProfileServiceMock.Object, LoggerMock.Object);
-            Assert.IsNull(decisionService.GetStoredVariation(experiment, storedUserProfile, ProjectConfig, DefaultDecisionReasons.NewInstance()));
+            Assert.IsNull(decisionService.GetStoredVariation(experiment, storedUserProfile, ProjectConfig));
             LoggerMock.Verify(l => l.Log(LogLevel.INFO, string.Format("User \"{0}\" was previously bucketed into variation with ID \"{1}\" for experiment \"{2}\", but no matching variation was found for that user. We will re-bucket the user."
                 , UserProfileId, storedVariationId, experiment.Id)), Times.Once);
         }
@@ -266,7 +265,7 @@ public void TestGetVariationSavesBucketedVariationIntoUserProfile()
                 });
 
             var mockBucketer = new Mock<Bucketer>(LoggerMock.Object);
-            mockBucketer.Setup(m => m.Bucket(ProjectConfig, experiment, UserProfileId, UserProfileId, It.IsAny<IDecisionReasons>())).Returns(variation);
+            mockBucketer.Setup(m => m.Bucket(ProjectConfig, experiment, UserProfileId, UserProfileId)).Returns(variation);
 
             DecisionService decisionService = new DecisionService(mockBucketer.Object, ErrorHandlerMock.Object, UserProfileServiceMock.Object, LoggerMock.Object);
 
@@ -318,7 +317,7 @@ public void TestGetVariationSavesANewUserProfile()
             });
 
             var mockBucketer = new Mock<Bucketer>(LoggerMock.Object);
-            mockBucketer.Setup(m => m.Bucket(ProjectConfig, experiment, UserProfileId, UserProfileId, It.IsAny<IDecisionReasons>())).Returns(variation);
+            mockBucketer.Setup(m => m.Bucket(ProjectConfig, experiment, UserProfileId, UserProfileId)).Returns(variation);
 
             Dictionary<string, object> userProfile = null;
 
@@ -469,7 +468,7 @@ public void TestGetVariationWithBucketingId()
         public void TestGetVariationForFeatureExperimentGivenNullExperimentIds()
         {
             var featureFlag = ProjectConfig.GetFeatureFlagFromKey("empty_feature");
-            var decision = DecisionService.GetVariationForFeatureExperiment(featureFlag, GenericUserId, new UserAttributes() { }, ProjectConfig, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance());
+            var decision = DecisionService.GetVariationForFeatureExperiment(featureFlag, GenericUserId, new UserAttributes() { }, ProjectConfig);
 
             Assert.IsNull(decision);
 
@@ -488,7 +487,7 @@ public void TestGetVariationForFeatureExperimentGivenExperimentNotInDataFile()
                 ExperimentIds = new List<string> { "29039203" }
             };
 
-            var decision = DecisionService.GetVariationForFeatureExperiment(featureFlag, GenericUserId, new UserAttributes() { }, ProjectConfig, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance());
+            var decision = DecisionService.GetVariationForFeatureExperiment(featureFlag, GenericUserId, new UserAttributes() { }, ProjectConfig);
             Assert.IsNull(decision);
 
             LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Experiment ID \"29039203\" is not in datafile."));
@@ -503,7 +502,7 @@ public void TestGetVariationForFeatureExperimentGivenNonMutexGroupAndUserNotBuck
             DecisionServiceMock.Setup(ds => ds.GetVariation(multiVariateExp, "user1", ProjectConfig, null)).Returns<Variation>(null);
             var featureFlag = ProjectConfig.GetFeatureFlagFromKey("multi_variate_feature");
 
-            var decision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", new UserAttributes(), ProjectConfig, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance());
+            var decision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", new UserAttributes(), ProjectConfig);
             Assert.IsNull(decision);
 
             LoggerMock.Verify(l => l.Log(LogLevel.INFO, "The user \"user1\" is not bucketed into any of the experiments on the feature \"multi_variate_feature\"."));
@@ -519,10 +518,10 @@ public void TestGetVariationForFeatureExperimentGivenNonMutexGroupAndUserIsBucke
             var userAttributes = new UserAttributes();
 
             DecisionServiceMock.Setup(ds => ds.GetVariation(ProjectConfig.GetExperimentFromKey("test_experiment_multivariate"),
-                "user1", ProjectConfig, userAttributes, It.IsAny<List<OptimizelyDecideOption>>(), It.IsAny<IDecisionReasons>())).Returns(variation);
+                "user1", ProjectConfig, userAttributes)).Returns(variation);
 
             var featureFlag = ProjectConfig.GetFeatureFlagFromKey("multi_variate_feature");
-            var decision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance());
+            var decision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig);
 
             Assert.IsTrue(TestData.CompareObjects(expectedDecision, decision));
 
@@ -542,7 +541,7 @@ public void TestGetVariationForFeatureExperimentGivenMutexGroupAndUserIsBucketed
                 userAttributes)).Returns(variation);
 
             var featureFlag = ProjectConfig.GetFeatureFlagFromKey("boolean_feature");
-            var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance());
+            var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig);
 
             Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision));
 
@@ -554,11 +553,11 @@ public void TestGetVariationForFeatureExperimentGivenMutexGroupAndUserIsBucketed
         public void TestGetVariationForFeatureExperimentGivenMutexGroupAndUserNotBucketed()
         {
             var mutexExperiment = ProjectConfig.GetExperimentFromKey("group_experiment_1");
-            DecisionServiceMock.Setup(ds => ds.GetVariation(It.IsAny<Experiment>(), It.IsAny<string>(), ProjectConfig, It.IsAny<UserAttributes>(), It.IsAny<List<OptimizelyDecideOption>>(), It.IsAny<IDecisionReasons>())).
+            DecisionServiceMock.Setup(ds => ds.GetVariation(It.IsAny<Experiment>(), It.IsAny<string>(), ProjectConfig, It.IsAny<UserAttributes>())).
                 Returns<Variation>(null);
 
             var featureFlag = ProjectConfig.GetFeatureFlagFromKey("boolean_feature");
-            var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", new UserAttributes(), ProjectConfig, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance());
+            var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", new UserAttributes(), ProjectConfig);
 
             Assert.IsNull(actualDecision);
 
@@ -583,7 +582,7 @@ public void TestGetVariationForFeatureRolloutWhenNoRuleInRollouts()
 
             var decisionService = new DecisionService(new Bucketer(new NoOpLogger()), new NoOpErrorHandler(), null, new NoOpLogger());
 
-            var variation = decisionService.GetVariationForFeatureRollout(featureFlag, "userId1", null, projectConfig, DefaultDecisionReasons.NewInstance());
+            var variation = decisionService.GetVariationForFeatureRollout(featureFlag, "userId1", null, projectConfig);
 
             Assert.IsNull(variation);
         }
@@ -601,9 +600,9 @@ public void TestGetVariationForFeatureRolloutWhenRolloutIsNotInDataFile()
                 Variables = featureFlag.Variables
             };
 
-            DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny<FeatureFlag>(), It.IsAny<string>(), It.IsAny<UserAttributes>(), ProjectConfig, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance())).Returns<Variation>(null);
+            DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny<FeatureFlag>(), It.IsAny<string>(), It.IsAny<UserAttributes>(), ProjectConfig)).Returns<Variation>(null);
 
-            var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureRollout(featureFlag, "user1", new UserAttributes(), ProjectConfig, DefaultDecisionReasons.NewInstance());
+            var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureRollout(featureFlag, "user1", new UserAttributes(), ProjectConfig);
             Assert.IsNull(actualDecision);
 
             LoggerMock.Verify(l => l.Log(LogLevel.INFO, "The feature flag \"boolean_feature\" is not used in a rollout."));
@@ -625,10 +624,10 @@ public void TestGetVariationForFeatureRolloutWhenUserIsBucketedInTheTargetingRul
             };
 
             BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(),
-                It.IsAny<string>(), It.IsAny<IDecisionReasons>())).Returns(variation);
+                It.IsAny<string>())).Returns(variation);
             var decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, null, LoggerMock.Object);
 
-            var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", userAttributes, ProjectConfig, DefaultDecisionReasons.NewInstance());
+            var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", userAttributes, ProjectConfig);
             Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision));
         }
 
@@ -649,11 +648,11 @@ public void TestGetVariationForFeatureRolloutWhenUserIsNotBucketedInTheTargeting
                 { "browser_type", "chrome" }
             };
 
-            BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), experiment, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>())).Returns<Variation>(null);
-            BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), everyoneElseRule, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>())).Returns(variation);
+            BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), experiment, It.IsAny<string>(), It.IsAny<string>())).Returns<Variation>(null);
+            BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), everyoneElseRule, It.IsAny<string>(), It.IsAny<string>())).Returns(variation);
             var decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, null, LoggerMock.Object);
 
-            var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", userAttributes, ProjectConfig, DefaultDecisionReasons.NewInstance());
+            var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", userAttributes, ProjectConfig);
             Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision));
         }
 
@@ -669,10 +668,10 @@ public void TestGetVariationForFeatureRolloutWhenUserIsNeitherBucketedInTheTarge
                 { "browser_type", "chrome" }
             };
 
-            BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>())).Returns<Variation>(null);
+            BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>())).Returns<Variation>(null);
             var decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, null, LoggerMock.Object);
 
-            var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", userAttributes, ProjectConfig, DefaultDecisionReasons.NewInstance());
+            var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", userAttributes, ProjectConfig);
             Assert.IsNull(actualDecision);
         }
 
@@ -690,11 +689,11 @@ public void TestGetVariationForFeatureRolloutWhenUserDoesNotQualifyForAnyTargeti
             var variation = everyoneElseRule.Variations[0];
             var expectedDecision = new FeatureDecision(everyoneElseRule, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT);
 
-            BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), everyoneElseRule, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>())).Returns(variation);
+            BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), everyoneElseRule, It.IsAny<string>(), It.IsAny<string>())).Returns(variation);
             var decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, null, LoggerMock.Object);
 
             // Provide null attributes so that user does not qualify for audience.
-            var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", null, ProjectConfig, DefaultDecisionReasons.NewInstance());
+            var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", null, ProjectConfig);
 
             Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision));
 
@@ -724,7 +723,7 @@ public void TestGetVariationForFeatureRolloutAudienceAndTrafficeAllocationCheck(
             {
                 { "device_type", "iPhone" },
                 { "location", "San Francisco" }
-            }, ProjectConfig, DefaultDecisionReasons.NewInstance());
+            }, ProjectConfig);
 
             // Returned variation id should be '177773' because of audience 'iPhone users in San Francisco'.
             var expectedDecision = new FeatureDecision(expWithAudienceiPhoneUsers, varWithAudienceiPhoneUsers, FeatureDecision.DECISION_SOURCE_ROLLOUT);
@@ -734,7 +733,7 @@ public void TestGetVariationForFeatureRolloutAudienceAndTrafficeAllocationCheck(
             actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, new UserAttributes
             {
                 { "browser_type", "chrome" }
-            }, ProjectConfig, DefaultDecisionReasons.NewInstance());
+            }, ProjectConfig);
 
             // Returned variation id should be '177771' because of audience 'Chrome users'.
             expectedDecision = new FeatureDecision(expWithAudienceChromeUsers, varWithAudienceChromeUsers, FeatureDecision.DECISION_SOURCE_ROLLOUT);
@@ -742,7 +741,7 @@ public void TestGetVariationForFeatureRolloutAudienceAndTrafficeAllocationCheck(
 
             // Calling with no audience.
             mockBucketer.Setup(bm => bm.GenerateBucketValue(It.IsAny<string>())).Returns(8000);
-            actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, new UserAttributes(), ProjectConfig, DefaultDecisionReasons.NewInstance());
+            actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, new UserAttributes(), ProjectConfig);
 
             // Returned variation id should be of everyone else rule because of no audience.
             expectedDecision = new FeatureDecision(expWithNoAudience, varWithNoAudience, FeatureDecision.DECISION_SOURCE_ROLLOUT);
@@ -753,7 +752,7 @@ public void TestGetVariationForFeatureRolloutAudienceAndTrafficeAllocationCheck(
             actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, new UserAttributes
             {
                 { "browser_type", "chrome" }
-            }, ProjectConfig, DefaultDecisionReasons.NewInstance());
+            }, ProjectConfig);
 
             // Returned decision entity should be null because bucket value exceeds traffice allocation of everyone else rule.
             Assert.Null(actualDecision);
@@ -769,21 +768,21 @@ public void TestGetVariationForFeatureRolloutCheckAudienceInEveryoneElseRule()
             var variation = everyoneElseRule.Variations[0];
             var expectedDecision = new FeatureDecision(everyoneElseRule, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT);
 
-            BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), everyoneElseRule, It.IsAny<string>(), WhitelistedUserId, It.IsAny<IDecisionReasons>())).Returns(variation);
-            BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), everyoneElseRule, It.IsAny<string>(), GenericUserId, It.IsAny<IDecisionReasons>())).Returns<Variation>(null);
+            BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), everyoneElseRule, It.IsAny<string>(), WhitelistedUserId)).Returns(variation);
+            BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), everyoneElseRule, It.IsAny<string>(), GenericUserId)).Returns<Variation>(null);
             var decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, null, LoggerMock.Object);
 
             // Returned variation id should be of everyone else rule as it passes audience Id checking.
-            var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, WhitelistedUserId, null, ProjectConfig, DefaultDecisionReasons.NewInstance());
+            var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, WhitelistedUserId, null, ProjectConfig);
             Assert.True(TestData.CompareObjects(expectedDecision, actualDecision));
 
             // Returned variation id should be null.
-            actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, null, ProjectConfig, DefaultDecisionReasons.NewInstance());
+            actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, null, ProjectConfig);
             Assert.Null(actualDecision);
 
             // Returned variation id should be null as it fails audience Id checking.
             everyoneElseRule.AudienceIds = new string[] { ProjectConfig.Audiences[0].Id };
-            actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, null, ProjectConfig, DefaultDecisionReasons.NewInstance());
+            actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, null, ProjectConfig);
             Assert.Null(actualDecision);
 
             LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "User \"testUser1\" does not meet the conditions for targeting rule \"1\"."), Times.Once);
@@ -808,7 +807,7 @@ public void TestGetVariationForFeatureWhenTheUserIsBucketedIntoFeatureExperiment
             var expectedDecision = new FeatureDecision(expectedExperiment, variation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST);
 
             DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny<FeatureFlag>(), It.IsAny<string>(),
-                It.IsAny<UserAttributes>(), ProjectConfig, It.IsAny<List<OptimizelyDecideOption>>(), It.IsAny<IDecisionReasons>())).Returns(expectedDecision);
+                It.IsAny<UserAttributes>(), ProjectConfig)).Returns(expectedDecision);
 
             var actualDecision = DecisionServiceMock.Object.GetVariationForFeature(featureFlag, "user1", ProjectConfig, new UserAttributes());
             Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision));
@@ -827,9 +826,9 @@ public void TestGetVariationForFeatureWhenTheUserIsNotBucketedIntoFeatureExperim
             var expectedDecision = new FeatureDecision(expectedExperiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT);
 
             DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny<FeatureFlag>(), It.IsAny<string>(),
-                It.IsAny<UserAttributes>(), ProjectConfig, It.IsAny<List<OptimizelyDecideOption>>(), It.IsAny<IDecisionReasons>())).Returns<Variation>(null);
+                It.IsAny<UserAttributes>(), ProjectConfig)).Returns<Variation>(null);
             DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureRollout(It.IsAny<FeatureFlag>(), It.IsAny<string>(),
-                It.IsAny<UserAttributes>(), ProjectConfig, It.IsAny<IDecisionReasons>())).Returns(expectedDecision);
+                It.IsAny<UserAttributes>(), ProjectConfig)).Returns(expectedDecision);
 
             var actualDecision = DecisionServiceMock.Object.GetVariationForFeature(featureFlag, "user1", ProjectConfig, new UserAttributes());
 
@@ -845,8 +844,8 @@ public void TestGetVariationForFeatureWhenTheUserIsNeitherBucketedIntoFeatureExp
             var featureFlag = ProjectConfig.GetFeatureFlagFromKey("string_single_variable_feature");
             var expectedDecision = new FeatureDecision(null, null, FeatureDecision.DECISION_SOURCE_ROLLOUT);
 
-            DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny<FeatureFlag>(), It.IsAny<string>(), It.IsAny<UserAttributes>(), ProjectConfig, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance())).Returns<Variation>(null);
-            DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureRollout(It.IsAny<FeatureFlag>(), It.IsAny<string>(), It.IsAny<UserAttributes>(), ProjectConfig, DefaultDecisionReasons.NewInstance())).Returns<Variation>(null);
+            DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny<FeatureFlag>(), It.IsAny<string>(), It.IsAny<UserAttributes>(), ProjectConfig)).Returns<Variation>(null);
+            DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureRollout(It.IsAny<FeatureFlag>(), It.IsAny<string>(), It.IsAny<UserAttributes>(), ProjectConfig)).Returns<Variation>(null);
 
             var actualDecision = DecisionServiceMock.Object.GetVariationForFeature(featureFlag, "user1", ProjectConfig, new UserAttributes());
             Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision));
@@ -867,8 +866,8 @@ public void TestGetVariationForFeatureWhenTheUserIsBuckedtedInBothExperimentAndR
                 { "browser_type", "chrome" }
             };
 
-            DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, "user1", ProjectConfig, userAttributes, It.IsAny<List<OptimizelyDecideOption>>(), It.IsAny<IDecisionReasons>())).Returns(variation);
-            var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance());
+            DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, "user1", ProjectConfig, userAttributes)).Returns(variation);
+            var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig);
 
             // The user is bucketed into feature experiment's variation.
             Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision));
@@ -878,8 +877,8 @@ public void TestGetVariationForFeatureWhenTheUserIsBuckedtedInBothExperimentAndR
             var rolloutVariation = rolloutExperiment.Variations[0];
             var expectedRolloutDecision = new FeatureDecision(rolloutExperiment, rolloutVariation, FeatureDecision.DECISION_SOURCE_ROLLOUT);
 
-            BucketerMock.Setup(bm => bm.Bucket(ProjectConfig, rolloutExperiment, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>())).Returns(rolloutVariation);
-            var actualRolloutDecision = DecisionServiceMock.Object.GetVariationForFeatureRollout(featureFlag, "user1", userAttributes, ProjectConfig, DefaultDecisionReasons.NewInstance());
+            BucketerMock.Setup(bm => bm.Bucket(ProjectConfig, rolloutExperiment, It.IsAny<string>(), It.IsAny<string>())).Returns(rolloutVariation);
+            var actualRolloutDecision = DecisionServiceMock.Object.GetVariationForFeatureRollout(featureFlag, "user1", userAttributes, ProjectConfig);
 
             // The user is bucketed into feature rollout's variation.
 
diff --git a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs
deleted file mode 100644
index 029c8499..00000000
--- a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs
+++ /dev/null
@@ -1,201 +0,0 @@
-/**
- *
- *    Copyright 2020, Optimizely and contributors
- *
- *    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 Castle.Core.Internal;
-using Moq;
-using NUnit.Framework;
-using OptimizelySDK.Entity;
-using OptimizelySDK.ErrorHandler;
-using OptimizelySDK.Event.Dispatcher;
-using OptimizelySDK.Logger;
-using OptimizelySDK.OptimizelyDecisions;
-using System;
-using System.Collections.Generic;
-
-namespace OptimizelySDK.Tests
-{
-    [TestFixture]
-    public class OptimizelyUserContextTest
-    {
-        string UserID = "testUserID";
-        private Optimizely Optimizely;
-        private Mock<ILogger> LoggerMock;
-        private Mock<IErrorHandler> ErrorHandlerMock;
-        private Mock<IEventDispatcher> EventDispatcherMock;
-
-        [SetUp]
-        public void SetUp()
-        {
-            LoggerMock = new Mock<ILogger>();
-            LoggerMock.Setup(i => i.Log(It.IsAny<LogLevel>(), It.IsAny<string>()));
-
-            ErrorHandlerMock = new Mock<IErrorHandler>();
-            ErrorHandlerMock.Setup(e => e.HandleError(It.IsAny<Exception>()));
-            EventDispatcherMock = new Mock<IEventDispatcher>();
-
-            Optimizely = new Optimizely(TestData.Datafile, EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object);
-        }
-
-        [Test]
-        public void OptimizelyUserContext_withAttributes()
-        {
-            var attributes = new UserAttributes() { { "house", "GRYFFINDOR" } };
-            OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object);
-
-            Assert.AreEqual(user.Optimizely, Optimizely);
-            Assert.AreEqual(user.UserId, UserID);
-            Assert.AreEqual(user.UserAttributes, attributes);
-        }
-
-        [Test]
-        public void OptimizelyUserContext_noAttributes()
-        {
-            OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, null, ErrorHandlerMock.Object, LoggerMock.Object);
-
-            Assert.AreEqual(user.Optimizely, Optimizely);
-            Assert.AreEqual(user.UserId, UserID);
-            Assert.True(user.UserAttributes.Count == 0);
-        }
-
-        [Test]
-        public void SetAttribute()
-        {
-            var attributes = new UserAttributes() { { "house", "GRYFFINDOR" } };
-            OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object);
-
-            user.SetAttribute("k1", "v1");
-            user.SetAttribute("k2", true);
-            user.SetAttribute("k3", 100);
-            user.SetAttribute("k4", 3.5);
-
-            Assert.AreEqual(user.Optimizely, Optimizely);
-            Assert.AreEqual(user.UserId, UserID);
-            var newAttributes = user.UserAttributes;
-            Assert.AreEqual(newAttributes["house"], "GRYFFINDOR");
-            Assert.AreEqual(newAttributes["k1"], "v1");
-            Assert.AreEqual(newAttributes["k2"], true);
-            Assert.AreEqual(newAttributes["k3"], 100);
-            Assert.AreEqual(newAttributes["k4"], 3.5);
-        }
-
-        [Test]
-        public void SetAttribute_noAttribute()
-        {
-            OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, null, ErrorHandlerMock.Object, LoggerMock.Object);
-
-            user.SetAttribute("k1", "v1");
-            user.SetAttribute("k2", true);
-
-            Assert.AreEqual(user.Optimizely, Optimizely);
-            Assert.AreEqual(user.UserId, UserID);
-            var newAttributes = user.UserAttributes;
-            Assert.AreEqual(newAttributes["k1"], "v1");
-            Assert.AreEqual(newAttributes["k2"], true);
-        }
-
-        [Test]
-        public void SetAttribute_override()
-        {
-            var attributes = new UserAttributes() { { "house", "GRYFFINDOR" } };
-            OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object);
-
-            user.SetAttribute("k1", "v1");
-            user.SetAttribute("house", "v2");
-
-            var newAttributes = user.UserAttributes;
-            Assert.AreEqual(newAttributes["k1"], "v1");
-            Assert.AreEqual(newAttributes["house"], "v2");
-        }
-
-        [Test]
-        public void SetAttribute_nullValue()
-        {
-            var attributes = new UserAttributes() { { "k1", null } };
-            OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object);
-
-            var newAttributes = user.UserAttributes;
-            Assert.AreEqual(newAttributes["k1"], null);
-
-            user.SetAttribute("k1", true);
-            newAttributes = user.UserAttributes;
-            Assert.AreEqual(newAttributes["k1"], true);
-
-            user.SetAttribute("k1", null);
-            newAttributes = user.UserAttributes;
-            Assert.AreEqual(newAttributes["k1"], null);
-        }
-
-        #region decide
-
-        [Test]
-        public void Decide()
-        {
-            var flagKey = "multi_variate_feature";
-            var variablesExpected = Optimizely.GetAllFeatureVariables(flagKey, UserID);
-
-            var user = Optimizely.CreateUserContext(UserID);
-            user.SetAttribute("browser_type", "chrome");
-            var decision = user.Decide(flagKey);
-
-            Assert.AreEqual(decision.VariationKey, "Gred");
-            Assert.False(decision.Enabled);
-            Assert.AreEqual(decision.Variables.ToDictionary(), variablesExpected.ToDictionary());
-            Assert.AreEqual(decision.RuleKey, "test_experiment_multivariate");
-            Assert.AreEqual(decision.FlagKey, flagKey);
-            Assert.AreEqual(decision.UserContext, user);
-            Assert.True(decision.Reasons.IsNullOrEmpty());
-        }
-
-        [Test]
-        public void DecideInvalidFlagKey()
-        {
-            var flagKey = "invalid_feature";
-
-            var user = Optimizely.CreateUserContext(UserID);
-            user.SetAttribute("browser_type", "chrome");
-            var decision = user.Decide(flagKey);
-
-            Assert.Null(decision.VariationKey);
-            Assert.False(decision.Enabled);
-            Assert.AreEqual(decision.Variables.ToDictionary(), new Dictionary<string, object>());
-            Assert.IsNull(decision.RuleKey);
-            Assert.AreEqual(decision.FlagKey, flagKey);
-            Assert.AreEqual(decision.UserContext, user);
-            Assert.True(decision.Reasons.IsNullOrEmpty());
-        }
-
-        [Test]
-        public void DecideInvalidConfig()
-        {
-            Optimizely optimizely = new Optimizely(TestData.UnsupportedVersionDatafile, EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object);
-
-            var flagKey = "multi_variate_feature";
-            var decisionExpected = OptimizelyDecision.NewErrorDecision(
-                flagKey,
-                new OptimizelyUserContext(optimizely, UserID, new UserAttributes(), ErrorHandlerMock.Object, LoggerMock.Object),
-                DecisionMessage.SDK_NOT_READY,
-                ErrorHandlerMock.Object,
-                LoggerMock.Object);
-            var user = optimizely.CreateUserContext(UserID);
-            var decision = user.Decide(flagKey);
-
-            Assert.IsTrue(TestData.CompareObjects(decision, decisionExpected));
-        }
-        #endregion
-
-    }
-}
diff --git a/OptimizelySDK/Bucketing/Bucketer.cs b/OptimizelySDK/Bucketing/Bucketer.cs
index be62727a..b8944687 100644
--- a/OptimizelySDK/Bucketing/Bucketer.cs
+++ b/OptimizelySDK/Bucketing/Bucketer.cs
@@ -15,7 +15,6 @@
  */
 using OptimizelySDK.Entity;
 using OptimizelySDK.Logger;
-using OptimizelySDK.OptimizelyDecisions;
 using System;
 using System.Collections.Generic;
 using System.Text;
@@ -103,9 +102,8 @@ private string FindBucket(string bucketingId, string userId, string parentId, IE
         /// <param name="experiment">Experiment Experiment in which user is to be bucketed</param>
         /// <param name="bucketingId">A customer-assigned value used to create the key for the murmur hash.</param>
         /// <param name="userId">User identifier</param>
-        /// <param name="reasons">Decision log messages.</param>
         /// <returns>Variation which will be shown to the user</returns>
-        public virtual Variation Bucket(ProjectConfig config, Experiment experiment, string bucketingId, string userId, IDecisionReasons reasons)
+        public virtual Variation Bucket(ProjectConfig config, Experiment experiment, string bucketingId, string userId)
         {
             string message;
             Variation variation;
@@ -124,26 +122,26 @@ public virtual Variation Bucket(ProjectConfig config, Experiment experiment, str
                 if (string.IsNullOrEmpty(userExperimentId))
                 {
                     message = $"User [{userId}] is in no experiment.";
-                    Logger.Log(LogLevel.INFO, reasons.AddInfo(message));
+                    Logger.Log(LogLevel.INFO, message);
                     return new Variation();
                 }
 
                 if (userExperimentId != experiment.Id)
                 {
                     message = $"User [{userId}] is not in experiment [{experiment.Key}] of group [{experiment.GroupId}].";
-                    Logger.Log(LogLevel.INFO, reasons.AddInfo(message));
+                    Logger.Log(LogLevel.INFO, message);
                     return new Variation();
                 }
 
                 message = $"User [{userId}] is in experiment [{experiment.Key}] of group [{experiment.GroupId}].";
-                Logger.Log(LogLevel.INFO, reasons.AddInfo(message));
+                Logger.Log(LogLevel.INFO, message);
             }
 
             // Bucket user if not in whitelist and in group (if any).
             string variationId = FindBucket(bucketingId, userId, experiment.Id, experiment.TrafficAllocation);
             if (string.IsNullOrEmpty(variationId))
             {
-                Logger.Log(LogLevel.INFO, reasons.AddInfo($"User [{userId}] is in no variation."));
+                Logger.Log(LogLevel.INFO, $"User [{userId}] is in no variation.");
                 return new Variation();
 
             }
@@ -151,7 +149,7 @@ public virtual Variation Bucket(ProjectConfig config, Experiment experiment, str
             // success!
             variation = config.GetVariationFromId(experiment.Key, variationId);
             message = $"User [{userId}] is in variation [{variation.Key}] of experiment [{experiment.Key}].";
-            Logger.Log(LogLevel.INFO, reasons.AddInfo(message));
+            Logger.Log(LogLevel.INFO, message);
             return variation;
         }
     }
diff --git a/OptimizelySDK/Bucketing/DecisionService.cs b/OptimizelySDK/Bucketing/DecisionService.cs
index 3973b13e..3959dbbd 100644
--- a/OptimizelySDK/Bucketing/DecisionService.cs
+++ b/OptimizelySDK/Bucketing/DecisionService.cs
@@ -19,7 +19,6 @@
 using OptimizelySDK.Entity;
 using OptimizelySDK.ErrorHandler;
 using OptimizelySDK.Logger;
-using OptimizelySDK.OptimizelyDecisions;
 using OptimizelySDK.Utils;
 
 namespace OptimizelySDK.Bucketing
@@ -84,43 +83,20 @@ public DecisionService(Bucketer bucketer, IErrorHandler errorHandler, UserProfil
         /// <param name = "userId" > The userId of the user.
         /// <param name = "filteredAttributes" > The user's attributes. This should be filtered to just attributes in the Datafile.</param>
         /// <returns>The Variation the user is allocated into.</returns>
-        public virtual Variation GetVariation(Experiment experiment,
-            string userId,
-            ProjectConfig config,
-            UserAttributes filteredAttributes)
-        {
-            return GetVariation(experiment, userId, config, filteredAttributes, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance());
-        }
-
-        /// <summary>
-        /// Get a Variation of an Experiment for a user to be allocated into.
-        /// </summary>
-        /// <param name = "experiment" > The Experiment the user will be bucketed into.</param>
-        /// <param name = "userId" > The userId of the user.
-        /// <param name = "filteredAttributes" > The user's attributes. This should be filtered to just attributes in the Datafile.</param>
-        /// <returns>The Variation the user is allocated into.</returns>
-        public virtual Variation GetVariation(Experiment experiment,
-            string userId,
-            ProjectConfig config,
-            UserAttributes filteredAttributes,
-            List<OptimizelyDecideOption> options,
-            IDecisionReasons reasons)
+        public virtual Variation GetVariation(Experiment experiment, string userId, ProjectConfig config, UserAttributes filteredAttributes)
         {
             if (!ExperimentUtils.IsExperimentActive(experiment, Logger)) return null;
 
             // check if a forced variation is set
-            var forcedVariation = GetForcedVariation(experiment.Key, userId, config, reasons);
+            var forcedVariation = GetForcedVariation(experiment.Key, userId, config);
             if (forcedVariation != null)
                 return forcedVariation;
 
-            var variation = GetWhitelistedVariation(experiment, userId, reasons);
+            var variation = GetWhitelistedVariation(experiment, userId);
 
             if (variation != null) return variation;
-            // fetch the user profile map from the user profile service
-            var ignoreUPS = options.Contains(OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE);
-
             UserProfile userProfile = null;
-            if (UserProfileService != null && !ignoreUPS)
+            if (UserProfileService != null)
             {
                 try
                 {
@@ -128,21 +104,21 @@ public virtual Variation GetVariation(Experiment experiment,
                     if (userProfileMap != null && UserProfileUtil.IsValidUserProfileMap(userProfileMap))
                     {
                         userProfile = UserProfileUtil.ConvertMapToUserProfile(userProfileMap);
-                        variation = GetStoredVariation(experiment, userProfile, config, reasons);
+                        variation = GetStoredVariation(experiment, userProfile, config);
                         if (variation != null) return variation;
                     }
                     else if (userProfileMap == null)
                     {
-                        Logger.Log(LogLevel.INFO, reasons.AddInfo("We were unable to get a user profile map from the UserProfileService."));
+                        Logger.Log(LogLevel.INFO, "We were unable to get a user profile map from the UserProfileService.");
                     }
                     else
                     {
-                        Logger.Log(LogLevel.ERROR, reasons.AddInfo("The UserProfileService returned an invalid map."));
+                        Logger.Log(LogLevel.ERROR, "The UserProfileService returned an invalid map.");
                     }
                 }
                 catch (Exception exception)
                 {
-                    Logger.Log(LogLevel.ERROR, reasons.AddInfo(exception.Message));
+                    Logger.Log(LogLevel.ERROR, exception.Message);
                     ErrorHandler.HandleError(new Exceptions.OptimizelyRuntimeException(exception.Message));
                 }
             }
@@ -150,16 +126,16 @@ public virtual Variation GetVariation(Experiment experiment,
             if (ExperimentUtils.DoesUserMeetAudienceConditions(config, experiment, filteredAttributes, LOGGING_KEY_TYPE_EXPERIMENT, experiment.Key, Logger))
             {
                 // Get Bucketing ID from user attributes.
-                string bucketingId = GetBucketingId(userId, filteredAttributes, reasons);
+                string bucketingId = GetBucketingId(userId, filteredAttributes);
 
-                variation = Bucketer.Bucket(config, experiment, bucketingId, userId, reasons);
+                variation = Bucketer.Bucket(config, experiment, bucketingId, userId);
 
                 if (variation != null && variation.Key != null)
                 {
                     if (UserProfileService != null)
                     {
                         var bucketerUserProfile = userProfile ?? new UserProfile(userId, new Dictionary<string, Decision>());
-                        SaveVariation(experiment, variation, bucketerUserProfile, reasons);
+                        SaveVariation(experiment, variation, bucketerUserProfile);
 
                     }
                     else
@@ -168,7 +144,7 @@ public virtual Variation GetVariation(Experiment experiment,
 
                 return variation;
             }
-            Logger.Log(LogLevel.INFO, reasons.AddInfo($"User \"{userId}\" does not meet conditions to be in experiment \"{experiment.Key}\"."));
+            Logger.Log(LogLevel.INFO, $"User \"{userId}\" does not meet conditions to be in experiment \"{experiment.Key}\".");
 
             return null;
         }
@@ -181,18 +157,6 @@ public virtual Variation GetVariation(Experiment experiment,
         /// <param name="config">Project Config</param>
         /// <returns>Variation entity which the given user and experiment should be forced into.</returns>
         public Variation GetForcedVariation(string experimentKey, string userId, ProjectConfig config)
-        {
-            return GetForcedVariation(experimentKey, userId, config, DefaultDecisionReasons.NewInstance());
-        }
-
-        /// <summary>
-        /// Gets the forced variation for the given user and experiment.  
-        /// </summary>
-        /// <param name="experimentKey">The experiment key</param>
-        /// <param name="userId">The user ID</param>
-        /// <param name="config">Project Config</param>
-        /// <returns>Variation entity which the given user and experiment should be forced into.</returns>
-        public Variation GetForcedVariation(string experimentKey, string userId, ProjectConfig config, IDecisionReasons reasons)
         {
             if (ForcedVariationMap.ContainsKey(userId) == false)
             {
@@ -227,7 +191,8 @@ public Variation GetForcedVariation(string experimentKey, string userId, Project
             // this case is logged in getVariationFromKey   
             if (string.IsNullOrEmpty(variationKey))
                 return null;
-            Logger.Log(LogLevel.DEBUG, reasons.AddInfo($@"Variation ""{variationKey}"" is mapped to experiment ""{experimentKey}"" and user ""{userId}"" in the forced variation map"));
+
+            Logger.Log(LogLevel.DEBUG, $@"Variation ""{variationKey}"" is mapped to experiment ""{experimentKey}"" and user ""{userId}"" in the forced variation map");
 
             Variation variation = config.GetVariationFromKey(experimentKey, variationKey);
 
@@ -283,27 +248,16 @@ public bool SetForcedVariation(string experimentKey, string userId, string varia
             Logger.Log(LogLevel.DEBUG, $@"Set variation ""{variationId}"" for experiment ""{experimentId}"" and user ""{userId}"" in the forced variation map.");
             return true;
         }
-        /// <summary>
-        /// Get the variation the user has been whitelisted into.
-        /// </summary>
-        /// <param name = "experiment" >in which user is to be bucketed.</param>
-        /// <param name = "userId" > User Identifier</param>
-        /// <returns>if the user is not whitelisted into any variation {@link Variation}
-        /// the user is bucketed into if the user has a specified whitelisted variation.</returns>
-        public Variation GetWhitelistedVariation(Experiment experiment, string userId)
-        {
-            return GetWhitelistedVariation(experiment, userId, DefaultDecisionReasons.NewInstance());
-        }
+
 
         /// <summary>
         /// Get the variation the user has been whitelisted into.
         /// </summary>
         /// <param name = "experiment" >in which user is to be bucketed.</param>
         /// <param name = "userId" > User Identifier</param>
-        /// <param name = "reasons" > Decision log messages.</param>
         /// <returns>if the user is not whitelisted into any variation {@link Variation}
         /// the user is bucketed into if the user has a specified whitelisted variation.</returns>
-        public Variation GetWhitelistedVariation(Experiment experiment, string userId, IDecisionReasons reasons)
+        public Variation GetWhitelistedVariation(Experiment experiment, string userId)
         {
             //if a user has a forced variation mapping, return the respective variation
             Dictionary<string, string> userIdToVariationKeyMap = experiment.UserIdToKeyVariations;
@@ -317,9 +271,9 @@ public Variation GetWhitelistedVariation(Experiment experiment, string userId, I
                 : null;
 
             if (forcedVariation != null)
-                Logger.Log(LogLevel.INFO, reasons.AddInfo($"User \"{userId}\" is forced in variation \"{forcedVariationKey}\"."));
+                Logger.Log(LogLevel.INFO, $"User \"{userId}\" is forced in variation \"{forcedVariationKey}\".");
             else
-                Logger.Log(LogLevel.ERROR, reasons.AddInfo($"Variation \"{forcedVariationKey}\" is not in the datafile. Not activating user \"{userId}\"."));
+                Logger.Log(LogLevel.ERROR, $"Variation \"{forcedVariationKey}\" is not in the datafile. Not activating user \"{userId}\".");
 
             return forcedVariation;
         }
@@ -330,7 +284,7 @@ public Variation GetWhitelistedVariation(Experiment experiment, string userId, I
         /// <param name = "experiment" > which the user was bucketed</param>
         /// <param name = "userProfile" > User profile of the user</param>
         /// <returns>The user was previously bucketed into.</returns>
-        public Variation GetStoredVariation(Experiment experiment, UserProfile userProfile, ProjectConfig config, IDecisionReasons reasons)
+        public Variation GetStoredVariation(Experiment experiment, UserProfile userProfile, ProjectConfig config)
         {
             // ---------- Check User Profile for Sticky Bucketing ----------
             // If a user profile instance is present then check it for a saved variation
@@ -342,7 +296,7 @@ public Variation GetStoredVariation(Experiment experiment, UserProfile userProfi
 
             if (decision == null)
             {
-                Logger.Log(LogLevel.INFO, reasons.AddInfo($"No previously activated variation of experiment \"{experimentKey}\" for user \"{userProfile.UserId}\" found in user profile."));
+                Logger.Log(LogLevel.INFO, $"No previously activated variation of experiment \"{experimentKey}\" for user \"{userProfile.UserId}\" found in user profile.");
                 return null;
             }
 
@@ -356,11 +310,11 @@ public Variation GetStoredVariation(Experiment experiment, UserProfile userProfi
 
                 if (savedVariation == null)
                 {
-                    Logger.Log(LogLevel.INFO, reasons.AddInfo($"User \"{userProfile.UserId}\" was previously bucketed into variation with ID \"{variationId}\" for experiment \"{experimentId}\", but no matching variation was found for that user. We will re-bucket the user."));
+                    Logger.Log(LogLevel.INFO, $"User \"{userProfile.UserId}\" was previously bucketed into variation with ID \"{variationId}\" for experiment \"{experimentId}\", but no matching variation was found for that user. We will re-bucket the user.");
                     return null;
                 }
 
-                Logger.Log(LogLevel.INFO, reasons.AddInfo($"Returning previously activated variation \"{savedVariation.Key}\" of experiment \"{experimentKey}\" for user \"{userProfile.UserId}\" from user profile."));
+                Logger.Log(LogLevel.INFO, $"Returning previously activated variation \"{savedVariation.Key}\" of experiment \"{experimentKey}\" for user \"{userProfile.UserId}\" from user profile.");
                 return savedVariation;
             }
             catch (Exception)
@@ -368,16 +322,6 @@ public Variation GetStoredVariation(Experiment experiment, UserProfile userProfi
                 return null;
             }
         }
-        /// <summary>
-        /// Save a { @link Variation } of an { @link Experiment } for a user in the {@link UserProfileService}.
-        /// </summary>
-        /// <param name = "experiment" > The experiment the user was buck</param>
-        /// <param name = "variation" > The Variation to save.</param>
-        /// <param name = "userProfile" > instance of the user information.</param>
-        public void SaveVariation(Experiment experiment, Variation variation, UserProfile userProfile)
-        {
-            SaveVariation(experiment, variation, userProfile, DefaultDecisionReasons.NewInstance());
-        }
 
         /// <summary>
         /// Save a { @link Variation } of an { @link Experiment } for a user in the {@link UserProfileService}.
@@ -385,7 +329,7 @@ public void SaveVariation(Experiment experiment, Variation variation, UserProfil
         /// <param name = "experiment" > The experiment the user was buck</param>
         /// <param name = "variation" > The Variation to save.</param>
         /// <param name = "userProfile" > instance of the user information.</param>
-        public void SaveVariation(Experiment experiment, Variation variation, UserProfile userProfile, IDecisionReasons reasons)
+        public void SaveVariation(Experiment experiment, Variation variation, UserProfile userProfile)
         {
             //only save if the user has implemented a user profile service
             if (UserProfileService == null)
@@ -407,15 +351,15 @@ public void SaveVariation(Experiment experiment, Variation variation, UserProfil
             try
             {
                 UserProfileService.Save(userProfile.ToMap());
-                Logger.Log(LogLevel.INFO, reasons.AddInfo($"Saved variation \"{variation.Id}\" of experiment \"{experiment.Id}\" for user \"{userProfile.UserId}\"."));
+                Logger.Log(LogLevel.INFO, $"Saved variation \"{variation.Id}\" of experiment \"{experiment.Id}\" for user \"{userProfile.UserId}\".");
             }
             catch (Exception exception)
             {
-                Logger.Log(LogLevel.ERROR, reasons.AddInfo($"Failed to save variation \"{variation.Id}\" of experiment \"{experiment.Id}\" for user \"{userProfile.UserId}\"."));
+                Logger.Log(LogLevel.ERROR, $"Failed to save variation \"{variation.Id}\" of experiment \"{experiment.Id}\" for user \"{userProfile.UserId}\".");
                 ErrorHandler.HandleError(new Exceptions.OptimizelyRuntimeException(exception.Message));
             }
         }
-       
+
         /// <summary>
         /// Try to bucket the user into a rollout rule.
         /// Evaluate the user for rules in priority order by seeing if the user satisfies the audience.
@@ -424,14 +368,9 @@ public void SaveVariation(Experiment experiment, Variation variation, UserProfil
         /// <param name = "featureFlag" >The feature flag the user wants to access.</param>
         /// <param name = "userId" >User Identifier</param>
         /// <param name = "filteredAttributes" >The user's attributes. This should be filtered to just attributes in the Datafile.</param>
-        /// <param name = "reasons" >Decision log messages.</param>
         /// <returns>null if the user is not bucketed into the rollout or if the feature flag was not attached to a rollout.
         /// otherwise the FeatureDecision entity</returns>
-        public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag featureFlag,
-            string userId,
-            UserAttributes filteredAttributes,
-            ProjectConfig config,
-            IDecisionReasons reasons)
+        public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag featureFlag, string userId, UserAttributes filteredAttributes, ProjectConfig config)
         {
             if (featureFlag == null)
             {
@@ -441,7 +380,7 @@ public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag feature
 
             if (string.IsNullOrEmpty(featureFlag.RolloutId))
             {
-                Logger.Log(LogLevel.INFO, reasons.AddInfo($"The feature flag \"{featureFlag.Key}\" is not used in a rollout."));
+                Logger.Log(LogLevel.INFO, $"The feature flag \"{featureFlag.Key}\" is not used in a rollout.");
                 return null;
             }
 
@@ -449,7 +388,7 @@ public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag feature
 
             if (string.IsNullOrEmpty(rollout.Id))
             {
-                Logger.Log(LogLevel.ERROR, reasons.AddInfo($"The rollout with id \"{featureFlag.RolloutId}\" is not found in the datafile for feature flag \"{featureFlag.Key}\""));
+                Logger.Log(LogLevel.ERROR, $"The rollout with id \"{featureFlag.RolloutId}\" is not found in the datafile for feature flag \"{featureFlag.Key}\"");
                 return null;
             }
 
@@ -461,16 +400,16 @@ public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag feature
             var rolloutRulesLength = rollout.Experiments.Count;
 
             // Get Bucketing ID from user attributes.
-            string bucketingId = GetBucketingId(userId, filteredAttributes, reasons);
+            string bucketingId = GetBucketingId(userId, filteredAttributes);
 
             // For all rules before the everyone else rule
             for (int i = 0; i < rolloutRulesLength - 1; i++)
             {
                 string loggingKey = (i + 1).ToString(); 
                 var rolloutRule = rollout.Experiments[i];
-                if (ExperimentUtils.DoesUserMeetAudienceConditions(config, rolloutRule, filteredAttributes, LOGGING_KEY_TYPE_RULE, loggingKey, reasons, Logger))
+                if (ExperimentUtils.DoesUserMeetAudienceConditions(config, rolloutRule, filteredAttributes, LOGGING_KEY_TYPE_RULE, loggingKey, Logger))
                 {
-                    variation = Bucketer.Bucket(config, rolloutRule, bucketingId, userId, reasons);
+                    variation = Bucketer.Bucket(config, rolloutRule, bucketingId, userId);
                     if (variation == null || string.IsNullOrEmpty(variation.Id))
                         break;
 
@@ -485,9 +424,9 @@ public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag feature
 
             // Get the last rule which is everyone else rule.
             var everyoneElseRolloutRule = rollout.Experiments[rolloutRulesLength - 1];
-            if (ExperimentUtils.DoesUserMeetAudienceConditions(config, everyoneElseRolloutRule, filteredAttributes, LOGGING_KEY_TYPE_RULE, "Everyone Else", reasons, Logger))
+            if (ExperimentUtils.DoesUserMeetAudienceConditions(config, everyoneElseRolloutRule, filteredAttributes, LOGGING_KEY_TYPE_RULE, "Everyone Else", Logger))
             {
-                variation = Bucketer.Bucket(config, everyoneElseRolloutRule, bucketingId, userId, reasons);
+                variation = Bucketer.Bucket(config, everyoneElseRolloutRule, bucketingId, userId);
                 if (variation != null && !string.IsNullOrEmpty(variation.Id))
                 {
                     Logger.Log(LogLevel.DEBUG, $"User \"{userId}\" meets conditions for targeting rule \"Everyone Else\".");
@@ -511,12 +450,7 @@ public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag feature
         /// <param name = "filteredAttributes" >The user's attributes. This should be filtered to just attributes in the Datafile.</param>
         /// <returns>null if the user is not bucketed into the rollout or if the feature flag was not attached to a rollout.
         /// Otherwise the FeatureDecision entity</returns>
-        public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag featureFlag,
-            string userId,
-            UserAttributes filteredAttributes,
-            ProjectConfig config,
-            List<OptimizelyDecideOption> options,
-            IDecisionReasons reasons)
+        public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag featureFlag, string userId, UserAttributes filteredAttributes, ProjectConfig config)
         {
             if (featureFlag == null)
             {
@@ -526,7 +460,7 @@ public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag feat
 
             if (featureFlag.ExperimentIds == null || featureFlag.ExperimentIds.Count == 0)
             {
-                Logger.Log(LogLevel.INFO, reasons.AddInfo($"The feature flag \"{featureFlag.Key}\" is not used in any experiments."));
+                Logger.Log(LogLevel.INFO, $"The feature flag \"{featureFlag.Key}\" is not used in any experiments.");
                 return null;
             }
 
@@ -537,16 +471,16 @@ public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag feat
                 if (string.IsNullOrEmpty(experiment.Key))
                     continue;
 
-                var variation = GetVariation(experiment, userId, config, filteredAttributes, options, reasons);
+                var variation = GetVariation(experiment, userId, config, filteredAttributes);
 
                 if (variation != null && !string.IsNullOrEmpty(variation.Id))
                 {
-                    Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is bucketed into experiment \"{experiment.Key}\" of feature \"{featureFlag.Key}\"."));
+                    Logger.Log(LogLevel.INFO, $"The user \"{userId}\" is bucketed into experiment \"{experiment.Key}\" of feature \"{featureFlag.Key}\".");
                     return new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST);
                 }
             }
 
-            Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is not bucketed into any of the experiments on the feature \"{featureFlag.Key}\"."));
+            Logger.Log(LogLevel.INFO, $"The user \"{userId}\" is not bucketed into any of the experiments on the feature \"{featureFlag.Key}\".");
             return null;
         }
 
@@ -559,44 +493,23 @@ public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag feat
         /// <returns>null if the user is not bucketed into any variation or the FeatureDecision entity if the user is 
         /// successfully bucketed.</returns>
         public virtual FeatureDecision GetVariationForFeature(FeatureFlag featureFlag, string userId, ProjectConfig config, UserAttributes filteredAttributes)
-        {
-            return GetVariationForFeature(featureFlag, userId, config, filteredAttributes, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance());
-        }
-
-        /// <summary>
-        /// Get the variation the user is bucketed into for the FeatureFlag
-        /// </summary>
-        /// <param name = "featureFlag" >The feature flag the user wants to access.</param>
-        /// <param name = "userId" >User Identifier</param>
-        /// <param name = "filteredAttributes" >The user's attributes. This should be filtered to just attributes in the Datafile.</param>
-        /// <param name = "filteredAttributes" >The user's attributes. This should be filtered to just attributes in the Datafile.</param>
-        /// <param name = "options" >An array of decision options.</param>
-        /// <param name = "reasons" >Decision log messages.</param>
-        /// <returns>null if the user is not bucketed into any variation or the FeatureDecision entity if the user is 
-        /// successfully bucketed.</returns>
-        public virtual FeatureDecision GetVariationForFeature(FeatureFlag featureFlag,
-            string userId,
-            ProjectConfig config,
-            UserAttributes filteredAttributes,
-            List<OptimizelyDecideOption> options, 
-            IDecisionReasons reasons)
         {
             // Check if the feature flag has an experiment and the user is bucketed into that experiment.
-            var decision = GetVariationForFeatureExperiment(featureFlag, userId, filteredAttributes, config, options, reasons);
+            var decision = GetVariationForFeatureExperiment(featureFlag, userId, filteredAttributes, config);
 
             if (decision != null)
                 return decision;
 
             // Check if the feature flag has rollout and the the user is bucketed into one of its rules.
-            decision = GetVariationForFeatureRollout(featureFlag, userId, filteredAttributes, config, reasons);
+            decision = GetVariationForFeatureRollout(featureFlag, userId, filteredAttributes, config);
 
             if (decision != null)
             {
-                Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is bucketed into a rollout for feature flag \"{featureFlag.Key}\"."));
+                Logger.Log(LogLevel.INFO, $"The user \"{userId}\" is bucketed into a rollout for feature flag \"{featureFlag.Key}\".");
                 return decision;
             }
 
-            Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is not bucketed into a rollout for feature flag \"{featureFlag.Key}\"."));
+            Logger.Log(LogLevel.INFO, $"The user \"{userId}\" is not bucketed into a rollout for feature flag \"{featureFlag.Key}\".");
             return new FeatureDecision(null, null, FeatureDecision.DECISION_SOURCE_ROLLOUT);
         }
 
@@ -606,7 +519,7 @@ public virtual FeatureDecision GetVariationForFeature(FeatureFlag featureFlag,
         /// <param name = "userId" >User Identifier</param>
         /// <param name = "filteredAttributes" >The user's attributes.</param>
         /// <returns>Bucketing Id if it is a string type in attributes, user Id otherwise.</returns>
-        private string GetBucketingId(string userId, UserAttributes filteredAttributes, IDecisionReasons reasons)
+        private string GetBucketingId(string userId, UserAttributes filteredAttributes)
         {
             string bucketingId = userId;
 
@@ -620,7 +533,7 @@ private string GetBucketingId(string userId, UserAttributes filteredAttributes,
                 }
                 else
                 {
-                    Logger.Log(LogLevel.WARN, reasons.AddInfo("BucketingID attribute is not a string. Defaulted to userId"));
+                    Logger.Log(LogLevel.WARN, "BucketingID attribute is not a string. Defaulted to userId");
                 }
             }
 
diff --git a/OptimizelySDK/Optimizely.cs b/OptimizelySDK/Optimizely.cs
index 49901a1c..b30d00ac 100644
--- a/OptimizelySDK/Optimizely.cs
+++ b/OptimizelySDK/Optimizely.cs
@@ -476,45 +476,45 @@ public virtual bool IsFeatureEnabled(string featureKey, string userId, UserAttri
                 return false;
 
             bool featureEnabled = false;
-                var sourceInfo = new Dictionary<string, string>();
-                var decision = DecisionService.GetVariationForFeature(featureFlag, userId, config, userAttributes);
-                var variation = decision.Variation;
-                var decisionSource = decision?.Source ?? FeatureDecision.DECISION_SOURCE_ROLLOUT;
+            var sourceInfo = new Dictionary<string, string>();
+            var decision = DecisionService.GetVariationForFeature(featureFlag, userId, config, userAttributes);
+            var variation = decision.Variation;
+            var decisionSource = decision?.Source ?? FeatureDecision.DECISION_SOURCE_ROLLOUT;
 
-                SendImpressionEvent(decision.Experiment, variation, userId, userAttributes, config, featureKey, decisionSource);
+            SendImpressionEvent(decision.Experiment, variation, userId, userAttributes, config, featureKey, decisionSource);
 
-                if (variation != null)
-                {
-                    featureEnabled = variation.FeatureEnabled.GetValueOrDefault();
+            if (variation != null)
+            {
+                featureEnabled = variation.FeatureEnabled.GetValueOrDefault();
 
-                    // This information is only necessary for feature tests.
-                    // For rollouts experiments and variations are an implementation detail only.
-                    if (decision.Source == FeatureDecision.DECISION_SOURCE_FEATURE_TEST)
-                    {
-                        sourceInfo["experimentKey"] = decision.Experiment.Key;
-                        sourceInfo["variationKey"] = variation.Key;
-                    }
-                    else
-                    {
-                        Logger.Log(LogLevel.INFO, $@"The user ""{userId}"" is not being experimented on feature ""{featureKey}"".");
-                    }
+                // This information is only necessary for feature tests.
+                // For rollouts experiments and variations are an implementation detail only.
+                if (decision.Source == FeatureDecision.DECISION_SOURCE_FEATURE_TEST)
+                {
+                    sourceInfo["experimentKey"] = decision.Experiment.Key;
+                    sourceInfo["variationKey"] = variation.Key;
                 }
-
-                if (featureEnabled == true)
-                    Logger.Log(LogLevel.INFO, $@"Feature flag ""{featureKey}"" is enabled for user ""{userId}"".");
                 else
-                    Logger.Log(LogLevel.INFO, $@"Feature flag ""{featureKey}"" is not enabled for user ""{userId}"".");
-
-                var decisionInfo = new Dictionary<string, object>
                 {
-                    { "featureKey", featureKey },
-                    { "featureEnabled", featureEnabled },
-                    { "source", decision.Source },
-                    { "sourceInfo", sourceInfo },
-                };
-
-                NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision, DecisionNotificationTypes.FEATURE, userId,
-                   userAttributes ?? new UserAttributes(), decisionInfo);
+                    Logger.Log(LogLevel.INFO, $@"The user ""{userId}"" is not being experimented on feature ""{featureKey}"".");
+                }
+            }
+
+            if (featureEnabled == true)
+                Logger.Log(LogLevel.INFO, $@"Feature flag ""{featureKey}"" is enabled for user ""{userId}"".");
+            else
+                Logger.Log(LogLevel.INFO, $@"Feature flag ""{featureKey}"" is not enabled for user ""{userId}"".");
+
+            var decisionInfo = new Dictionary<string, object>
+            {
+                { "featureKey", featureKey },
+                { "featureEnabled", featureEnabled },
+                { "source", decision.Source },
+                { "sourceInfo", sourceInfo },
+            };
+
+            NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision, DecisionNotificationTypes.FEATURE, userId,
+               userAttributes ?? new UserAttributes(), decisionInfo);
             return featureEnabled;
         }
 
@@ -712,139 +712,6 @@ public OptimizelyUserContext CreateUserContext(string userId,
             return new OptimizelyUserContext(this, userId, userAttributes, ErrorHandler, Logger);
         }
 
-        public OptimizelyDecision Decide(OptimizelyUserContext user,
-                              string key,
-                              List<OptimizelyDecideOption> options)
-        {
-
-            var config = ProjectConfigManager?.GetConfig();
-            if (config == null)
-            {
-                return OptimizelyDecision.NewErrorDecision(key, user, DecisionMessage.SDK_NOT_READY, ErrorHandler, Logger);
-            }
-            var userId = user.UserId;
-            var inputValues = new Dictionary<string, string>
-            {
-                { USER_ID, userId },
-            };
-
-            if (!ValidateStringInputs(inputValues))
-                return null;
-
-            var flag = config.GetFeatureFlagFromKey(key);
-            if (flag == null)
-            {
-                return OptimizelyDecision.NewErrorDecision(key,
-                    user,
-                    DecisionMessage.Reason(DecisionMessage.FLAG_KEY_INVALID, key),
-                    ErrorHandler, Logger);
-            }
-
-            var userAttributes = user.UserAttributes;
-            var decisionEventDispatched = false;
-            var allOptions = GetAllOptions(options);
-            var decisionReasons = DefaultDecisionReasons.NewInstance(allOptions);
-
-            var flagDecision = DecisionService.GetVariationForFeature(
-                flag,
-                userId,
-                config,
-                userAttributes,
-                allOptions,
-                decisionReasons);
-
-            var featureEnabled = false;
-
-            var variation = flagDecision.Variation;
-
-            if (variation != null)
-            {
-                featureEnabled = variation.FeatureEnabled.GetValueOrDefault();
-            }
-            
-            if (featureEnabled)
-            {
-                Logger.Log(LogLevel.INFO, "Feature \"" + key + "\" is enabled for user \"" + userId + "\"");
-            }
-            else
-            {
-                Logger.Log(LogLevel.INFO, "Feature \"" + key + "\" is not enabled for user \"" + userId + "\"");
-            }
-            var variableMap = new Dictionary<string, object>();
-            if (flag?.Variables != null && !allOptions.Contains(OptimizelyDecideOption.EXCLUDE_VARIABLES))
-            {
-
-                foreach (var featureVariable in flag?.Variables)
-                {
-                    string variableValue = featureVariable.DefaultValue;
-                    if (featureEnabled)
-                    {
-                        var featureVariableUsageInstance = variation.GetFeatureVariableUsageFromId(featureVariable.Id);
-                        if (featureVariableUsageInstance != null)
-                        {
-                            variableValue = featureVariableUsageInstance.Value;
-                        }
-                    }
-
-                    var typeCastedValue = GetTypeCastedVariableValue(variableValue, featureVariable.Type);
-
-                    if (typeCastedValue is OptimizelyJSON)
-                        typeCastedValue = ((OptimizelyJSON)typeCastedValue).ToDictionary();
-
-                    variableMap.Add(featureVariable.Key, typeCastedValue);
-                }
-            }
-            
-            var optimizelyJSON = new OptimizelyJSON(variableMap, ErrorHandler, Logger);
-
-            var decisionSource = flagDecision?.Source ?? FeatureDecision.DECISION_SOURCE_ROLLOUT;
-            if (!allOptions.Contains(OptimizelyDecideOption.DISABLE_DECISION_EVENT))
-            {
-
-                SendImpressionEvent(flagDecision.Experiment, variation, userId, userAttributes, config, key, decisionSource);
-                decisionEventDispatched = true;
-            }
-            List<string> reasonsToReport = decisionReasons.ToReport();
-            string variationKey = flagDecision.Variation?.Key;
-
-            // TODO: add ruleKey values when available later. use a copy of experimentKey until then.
-            string ruleKey = flagDecision.Experiment?.Key;
-
-            var decisionInfo = new Dictionary<string, object>
-            {
-                { "flagKey", key },
-                { "enabled", featureEnabled },
-                { "variables", variableMap },
-                { "variationKey", variationKey },
-                { "ruleKey", ruleKey },
-                { "reasons", decisionReasons },
-                { "decisionEventDispatched", decisionEventDispatched },
-                { "featureEnabled", featureEnabled },
-            };
-
-            NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision, DecisionNotificationTypes.FEATURE, userId,
-               userAttributes ?? new UserAttributes(), decisionInfo);
-
-            return new OptimizelyDecision(
-                variationKey,
-                featureEnabled,
-                optimizelyJSON,
-                ruleKey,
-                key,
-                user,
-                reasonsToReport);
-        }
-
-        private List<OptimizelyDecideOption> GetAllOptions(List<OptimizelyDecideOption> options)
-        {
-            var copiedOptions = new List<OptimizelyDecideOption>(DefaultDecideOptions);
-            if (options != null)
-            {
-                copiedOptions.AddRange(options);
-            }
-            return copiedOptions;
-        }
-
         /// <summary>
         /// Sends impression event.
         /// </summary>
diff --git a/OptimizelySDK/OptimizelyUserContext.cs b/OptimizelySDK/OptimizelyUserContext.cs
index 07834546..e2621f20 100644
--- a/OptimizelySDK/OptimizelyUserContext.cs
+++ b/OptimizelySDK/OptimizelyUserContext.cs
@@ -46,7 +46,7 @@ public OptimizelyUserContext(Optimizely optimizely, string userId, UserAttribute
         /// <param name="value">value An attribute value</param>
         public void SetAttribute(string key, object value)
         {
-            UserAttributes[key] = value;
+            UserAttributes.Add(key, value);
         }
 
         /// <summary>
@@ -74,7 +74,7 @@ public OptimizelyDecision Decide(string key)
         public OptimizelyDecision Decide(string key,
             List<OptimizelyDecideOption> options)
         {
-            return Optimizely.Decide(this, key, options);
+            return null;
         }
  
         /// <summary>
diff --git a/OptimizelySDK/Utils/ExperimentUtils.cs b/OptimizelySDK/Utils/ExperimentUtils.cs
index b6023480..470b014c 100644
--- a/OptimizelySDK/Utils/ExperimentUtils.cs
+++ b/OptimizelySDK/Utils/ExperimentUtils.cs
@@ -17,7 +17,6 @@
 using OptimizelySDK.AudienceConditions;
 using OptimizelySDK.Entity;
 using OptimizelySDK.Logger;
-using OptimizelySDK.OptimizelyDecisions;
 
 namespace OptimizelySDK.Utils
 {
@@ -36,24 +35,6 @@ public static bool IsExperimentActive(Experiment experiment, ILogger logger)
             return true;
         }
 
-        /// <summary>
-        /// Check if the user meets audience conditions to be in experiment or not
-        /// </summary>
-        /// <param name="config">ProjectConfig Configuration for the project</param>
-        /// <param name="experiment">Experiment Entity representing the experiment</param>
-        /// <param name="userAttributes">Attributes of the user. Defaults to empty attributes array if not provided</param>
-        /// <param name="loggingKeyType">It can be either experiment or rule.</param>
-        /// <param name="loggingKey">In case loggingKeyType is experiment it will be experiment key or else it will be rule number.</param>
-        /// <returns>true if the user meets audience conditions to be in experiment, false otherwise.</returns>
-        public static bool DoesUserMeetAudienceConditions(ProjectConfig config,
-            Experiment experiment,
-            UserAttributes userAttributes,
-            string loggingKeyType,
-            string loggingKey,
-            ILogger logger)
-        {
-            return DoesUserMeetAudienceConditions(config, experiment, userAttributes, loggingKeyType, loggingKey, DefaultDecisionReasons.NewInstance(), logger);
-        }
 
         /// <summary>
         /// Check if the user meets audience conditions to be in experiment or not
@@ -63,14 +44,12 @@ public static bool DoesUserMeetAudienceConditions(ProjectConfig config,
         /// <param name="userAttributes">Attributes of the user. Defaults to empty attributes array if not provided</param>
         /// <param name="loggingKeyType">It can be either experiment or rule.</param>
         /// <param name="loggingKey">In case loggingKeyType is experiment it will be experiment key or else it will be rule number.</param>
-        /// <param name="reasons">Decision log messages.</param>
         /// <returns>true if the user meets audience conditions to be in experiment, false otherwise.</returns>
         public static bool DoesUserMeetAudienceConditions(ProjectConfig config,
             Experiment experiment,
             UserAttributes userAttributes,
             string loggingKeyType,
             string loggingKey,
-            IDecisionReasons reasons,
             ILogger logger)
         {
             if (userAttributes == null)
@@ -94,7 +73,7 @@ public static bool DoesUserMeetAudienceConditions(ProjectConfig config,
 
             var result = expConditions.Evaluate(config, userAttributes, logger).GetValueOrDefault();
             var resultText = result.ToString().ToUpper();
-            logger.Log(LogLevel.INFO, reasons.AddInfo($@"Audiences for {loggingKeyType} ""{loggingKey}"" collectively evaluated to {resultText}"));
+            logger.Log(LogLevel.INFO, $@"Audiences for {loggingKeyType} ""{loggingKey}"" collectively evaluated to {resultText}");
             return result;
         }
     }

From 8c783301ee565b7e960b69bd0d1f9919ebfedef8 Mon Sep 17 00:00:00 2001
From: muhammadnoman <muhammadnoman@folio3.com>
Date: Wed, 11 Nov 2020 23:08:32 +0500
Subject: [PATCH 09/19] Added UserContext add reason logs BugFix of
 userAttributes Unit tests fix

---
 OptimizelySDK.Tests/BucketerTest.cs           |  23 +-
 OptimizelySDK.Tests/DecisionServiceTest.cs    |  93 ++++----
 .../OptimizelyUserContextTest.cs              | 201 ++++++++++++++++++
 OptimizelySDK/Bucketing/Bucketer.cs           |  14 +-
 OptimizelySDK/Bucketing/DecisionService.cs    | 177 +++++++++++----
 OptimizelySDK/Optimizely.cs                   | 199 ++++++++++++++---
 OptimizelySDK/OptimizelyUserContext.cs        |   4 +-
 OptimizelySDK/Utils/ExperimentUtils.cs        |  23 +-
 8 files changed, 590 insertions(+), 144 deletions(-)
 create mode 100644 OptimizelySDK.Tests/OptimizelyUserContextTest.cs

diff --git a/OptimizelySDK.Tests/BucketerTest.cs b/OptimizelySDK.Tests/BucketerTest.cs
index aeeeb96c..78b0feec 100644
--- a/OptimizelySDK.Tests/BucketerTest.cs
+++ b/OptimizelySDK.Tests/BucketerTest.cs
@@ -19,6 +19,7 @@
 using OptimizelySDK.Config;
 using OptimizelySDK.Entity;
 using OptimizelySDK.Logger;
+using OptimizelySDK.OptimizelyDecisions;
 
 namespace OptimizelySDK.Tests
 {
@@ -98,7 +99,7 @@ public void TestBucketValidExperimentNotInGroup()
 
             // control
             Assert.AreEqual(new Variation { Id = "7722370027", Key = "control" },
-                bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId));
+                bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance()));
 
             LoggerMock.Verify(l => l.Log(It.IsAny<LogLevel>(), It.IsAny<string>()), Times.Exactly(2));
             LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [3000] to user [testUserId] with bucketing ID [testBucketingIdControl!]."));
@@ -106,7 +107,7 @@ public void TestBucketValidExperimentNotInGroup()
 
             // variation
             Assert.AreEqual(new Variation { Id = "7721010009", Key = "variation" },
-                bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId));
+                bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance()));
 
             LoggerMock.Verify(l => l.Log(It.IsAny<LogLevel>(), It.IsAny<string>()), Times.Exactly(4));
             LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [7000] to user [testUserId] with bucketing ID [testBucketingIdControl!]."));
@@ -114,7 +115,7 @@ public void TestBucketValidExperimentNotInGroup()
 
             // no variation
             Assert.AreEqual(new Variation { },
-                bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId));
+                bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance()));
 
             LoggerMock.Verify(l => l.Log(It.IsAny<LogLevel>(), It.IsAny<string>()), Times.Exactly(6));
             LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [9000] to user [testUserId] with bucketing ID [testBucketingIdControl!]."));
@@ -130,7 +131,7 @@ public void TestBucketValidExperimentInGroup()
             // variation 1
             bucketer.SetBucketValues(new[] { 1000, 4000 });
             Assert.AreEqual(new Variation { Id = "7722260071", Key = "group_exp_1_var_1" },
-                bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId));
+                bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance()));
             LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [1000] to user [testUserId] with bucketing ID [testBucketingIdControl!]."));
             LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [testUserId] is in experiment [group_experiment_1] of group [7722400015]."));
             LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [4000] to user [testUserId] with bucketing ID [testBucketingIdControl!]."));
@@ -139,7 +140,7 @@ public void TestBucketValidExperimentInGroup()
             // variation 2
             bucketer.SetBucketValues(new[] { 1500, 7000 });
             Assert.AreEqual(new Variation { Id = "7722360022", Key = "group_exp_1_var_2" },
-                bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId));
+                bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance()));
             LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [1500] to user [testUserId] with bucketing ID [testBucketingIdControl!]."));
             LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [testUserId] is in experiment [group_experiment_1] of group [7722400015]."));
             LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [7000] to user [testUserId] with bucketing ID [testBucketingIdControl!]."));
@@ -148,7 +149,7 @@ public void TestBucketValidExperimentInGroup()
             // User not in experiment
             bucketer.SetBucketValues(new[] { 5000, 7000 });
             Assert.AreEqual(new Variation { },
-                bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId));
+                bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance()));
             LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [5000] to user [testUserId] with bucketing ID [testBucketingIdControl!]."));
             LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [testUserId] is not in experiment [group_experiment_1] of group [7722400015]."));
 
@@ -161,7 +162,7 @@ public void TestBucketInvalidExperiment()
             var bucketer = new Bucketer(LoggerMock.Object);
 
             Assert.AreEqual(new Variation { },
-                bucketer.Bucket(Config, new Experiment(), TestBucketingIdControl, TestUserId));
+                bucketer.Bucket(Config, new Experiment(), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance()));
 
             LoggerMock.Verify(l => l.Log(It.IsAny<LogLevel>(), It.IsAny<string>()), Times.Never);
         }
@@ -176,7 +177,7 @@ public void TestBucketWithBucketingId()
 
             // make sure that the bucketing ID is used for the variation bucketing and not the user ID
             Assert.AreEqual(expectedVariation, 
-                bucketer.Bucket(Config, experiment, TestBucketingIdControl, TestUserIdBucketsToVariation));
+                bucketer.Bucket(Config, experiment, TestBucketingIdControl, TestUserIdBucketsToVariation, DefaultDecisionReasons.NewInstance()));
         }
 
         // Test for invalid experiment keys, null variation should be returned
@@ -187,7 +188,7 @@ public void TestBucketVariationInvalidExperimentsWithBucketingId()
             var expectedVariation = new Variation();
 
             Assert.AreEqual(expectedVariation, 
-                bucketer.Bucket(Config, Config.GetExperimentFromKey("invalid_experiment"), TestBucketingIdVariation, TestUserId));
+                bucketer.Bucket(Config, Config.GetExperimentFromKey("invalid_experiment"), TestBucketingIdVariation, TestUserId, DefaultDecisionReasons.NewInstance()));
         }
 
         // Make sure that the bucketing ID is used to bucket the user into a group and not the user ID
@@ -200,7 +201,7 @@ public void TestBucketVariationGroupedExperimentsWithBucketingId()
 
             Assert.AreEqual(expectedGroupVariation,
                 bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_2"), 
-                TestBucketingIdGroupExp2Var2, TestUserIdBucketsToNoGroup));
+                TestBucketingIdGroupExp2Var2, TestUserIdBucketsToNoGroup, DefaultDecisionReasons.NewInstance()));
         }
 
         // Make sure that user gets bucketed into the rollout rule.
@@ -213,7 +214,7 @@ public void TestBucketRolloutRule()
             var expectedVariation = Config.GetVariationFromId(rolloutRule.Key, "177773");
 
             Assert.True(TestData.CompareObjects(expectedVariation, 
-                bucketer.Bucket(Config, rolloutRule, "testBucketingId", TestUserId)));
+                bucketer.Bucket(Config, rolloutRule, "testBucketingId", TestUserId, DefaultDecisionReasons.NewInstance())));
         }
     }
 }
\ No newline at end of file
diff --git a/OptimizelySDK.Tests/DecisionServiceTest.cs b/OptimizelySDK.Tests/DecisionServiceTest.cs
index 0936a857..3c83b3db 100644
--- a/OptimizelySDK.Tests/DecisionServiceTest.cs
+++ b/OptimizelySDK.Tests/DecisionServiceTest.cs
@@ -24,6 +24,7 @@
 using OptimizelySDK.Bucketing;
 using OptimizelySDK.Utils;
 using OptimizelySDK.Config;
+using OptimizelySDK.OptimizelyDecisions;
 
 namespace OptimizelySDK.Tests
 {
@@ -84,7 +85,7 @@ public void TestGetVariationForcedVariationPrecedesAudienceEval()
             LoggerMock.Verify(l => l.Log(LogLevel.INFO, string.Format("User \"{0}\" is forced in variation \"vtag5\".", WhitelistedUserId)), Times.Once);
             // no attributes provided for a experiment that has an audience
             Assert.IsTrue(TestData.CompareObjects(actualVariation, expectedVariation));
-            BucketerMock.Verify(_ => _.Bucket(It.IsAny<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never);
+            BucketerMock.Verify(_ => _.Bucket(It.IsAny<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>()), Times.Never);
         }
 
         [Test]
@@ -111,7 +112,7 @@ public void TestGetVariationEvaluatesUserProfileBeforeAudienceTargeting()
             // ensure that a user with a saved user profile, sees the same variation regardless of audience evaluation
             decisionService.GetVariation(experiment, UserProfileId, ProjectConfig, new UserAttributes());
 
-            BucketerMock.Verify(_ => _.Bucket(It.IsAny<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never);
+            BucketerMock.Verify(_ => _.Bucket(It.IsAny<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>()), Times.Never);
         }
 
         [Test]
@@ -123,7 +124,7 @@ public void TestGetForcedVariationReturnsForcedVariation()
             LoggerMock.Verify(l => l.Log(LogLevel.INFO, string.Format("User \"{0}\" is forced in variation \"{1}\".",
                 WhitelistedUserId, WhitelistedVariation.Key)), Times.Once);
 
-            BucketerMock.Verify(_ => _.Bucket(It.IsAny<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never);
+            BucketerMock.Verify(_ => _.Bucket(It.IsAny<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>()), Times.Never);
         }
 
         [Test]
@@ -164,7 +165,7 @@ public void TestGetForcedVariationWithInvalidVariation()
                 string.Format("Variation \"{0}\" is not in the datafile. Not activating user \"{1}\".", invalidVariationKey, userId)),
                 Times.Once);
 
-            BucketerMock.Verify(_ => _.Bucket(It.IsAny<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never);
+            BucketerMock.Verify(_ => _.Bucket(It.IsAny<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>()), Times.Never);
         }
 
         [Test]
@@ -216,7 +217,7 @@ public void TestGetStoredVariationLogsWhenLookupReturnsNull()
             DecisionService decisionService = new DecisionService(bucketer,
                  ErrorHandlerMock.Object, userProfileService, LoggerMock.Object);
 
-            Assert.IsNull(decisionService.GetStoredVariation(experiment, userProfile, ProjectConfig));
+            Assert.IsNull(decisionService.GetStoredVariation(experiment, userProfile, ProjectConfig, DefaultDecisionReasons.NewInstance()));
 
             LoggerMock.Verify(l => l.Log(LogLevel.INFO, string.Format("No previously activated variation of experiment \"{0}\" for user \"{1}\" found in user profile."
                 , experiment.Key, UserProfileId)), Times.Once);
@@ -241,7 +242,7 @@ public void TestGetStoredVariationReturnsNullWhenVariationIsNoLongerInConfig()
 
             DecisionService decisionService = new DecisionService(bucketer, ErrorHandlerMock.Object,
                 UserProfileServiceMock.Object, LoggerMock.Object);
-            Assert.IsNull(decisionService.GetStoredVariation(experiment, storedUserProfile, ProjectConfig));
+            Assert.IsNull(decisionService.GetStoredVariation(experiment, storedUserProfile, ProjectConfig, DefaultDecisionReasons.NewInstance()));
             LoggerMock.Verify(l => l.Log(LogLevel.INFO, string.Format("User \"{0}\" was previously bucketed into variation with ID \"{1}\" for experiment \"{2}\", but no matching variation was found for that user. We will re-bucket the user."
                 , UserProfileId, storedVariationId, experiment.Id)), Times.Once);
         }
@@ -265,7 +266,7 @@ public void TestGetVariationSavesBucketedVariationIntoUserProfile()
                 });
 
             var mockBucketer = new Mock<Bucketer>(LoggerMock.Object);
-            mockBucketer.Setup(m => m.Bucket(ProjectConfig, experiment, UserProfileId, UserProfileId)).Returns(variation);
+            mockBucketer.Setup(m => m.Bucket(ProjectConfig, experiment, UserProfileId, UserProfileId, It.IsAny<IDecisionReasons>())).Returns(variation);
 
             DecisionService decisionService = new DecisionService(mockBucketer.Object, ErrorHandlerMock.Object, UserProfileServiceMock.Object, LoggerMock.Object);
 
@@ -317,7 +318,7 @@ public void TestGetVariationSavesANewUserProfile()
             });
 
             var mockBucketer = new Mock<Bucketer>(LoggerMock.Object);
-            mockBucketer.Setup(m => m.Bucket(ProjectConfig, experiment, UserProfileId, UserProfileId)).Returns(variation);
+            mockBucketer.Setup(m => m.Bucket(ProjectConfig, experiment, UserProfileId, UserProfileId, It.IsAny<IDecisionReasons>())).Returns(variation);
 
             Dictionary<string, object> userProfile = null;
 
@@ -468,7 +469,7 @@ public void TestGetVariationWithBucketingId()
         public void TestGetVariationForFeatureExperimentGivenNullExperimentIds()
         {
             var featureFlag = ProjectConfig.GetFeatureFlagFromKey("empty_feature");
-            var decision = DecisionService.GetVariationForFeatureExperiment(featureFlag, GenericUserId, new UserAttributes() { }, ProjectConfig);
+            var decision = DecisionService.GetVariationForFeatureExperiment(featureFlag, GenericUserId, new UserAttributes() { }, ProjectConfig, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance());
 
             Assert.IsNull(decision);
 
@@ -487,7 +488,7 @@ public void TestGetVariationForFeatureExperimentGivenExperimentNotInDataFile()
                 ExperimentIds = new List<string> { "29039203" }
             };
 
-            var decision = DecisionService.GetVariationForFeatureExperiment(featureFlag, GenericUserId, new UserAttributes() { }, ProjectConfig);
+            var decision = DecisionService.GetVariationForFeatureExperiment(featureFlag, GenericUserId, new UserAttributes() { }, ProjectConfig, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance());
             Assert.IsNull(decision);
 
             LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Experiment ID \"29039203\" is not in datafile."));
@@ -502,7 +503,7 @@ public void TestGetVariationForFeatureExperimentGivenNonMutexGroupAndUserNotBuck
             DecisionServiceMock.Setup(ds => ds.GetVariation(multiVariateExp, "user1", ProjectConfig, null)).Returns<Variation>(null);
             var featureFlag = ProjectConfig.GetFeatureFlagFromKey("multi_variate_feature");
 
-            var decision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", new UserAttributes(), ProjectConfig);
+            var decision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", new UserAttributes(), ProjectConfig, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance());
             Assert.IsNull(decision);
 
             LoggerMock.Verify(l => l.Log(LogLevel.INFO, "The user \"user1\" is not bucketed into any of the experiments on the feature \"multi_variate_feature\"."));
@@ -518,10 +519,10 @@ public void TestGetVariationForFeatureExperimentGivenNonMutexGroupAndUserIsBucke
             var userAttributes = new UserAttributes();
 
             DecisionServiceMock.Setup(ds => ds.GetVariation(ProjectConfig.GetExperimentFromKey("test_experiment_multivariate"),
-                "user1", ProjectConfig, userAttributes)).Returns(variation);
+                "user1", ProjectConfig, userAttributes, It.IsAny<List<OptimizelyDecideOption>>(), It.IsAny<IDecisionReasons>())).Returns(variation);
 
             var featureFlag = ProjectConfig.GetFeatureFlagFromKey("multi_variate_feature");
-            var decision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig);
+            var decision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance());
 
             Assert.IsTrue(TestData.CompareObjects(expectedDecision, decision));
 
@@ -541,7 +542,7 @@ public void TestGetVariationForFeatureExperimentGivenMutexGroupAndUserIsBucketed
                 userAttributes)).Returns(variation);
 
             var featureFlag = ProjectConfig.GetFeatureFlagFromKey("boolean_feature");
-            var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig);
+            var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance());
 
             Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision));
 
@@ -553,11 +554,11 @@ public void TestGetVariationForFeatureExperimentGivenMutexGroupAndUserIsBucketed
         public void TestGetVariationForFeatureExperimentGivenMutexGroupAndUserNotBucketed()
         {
             var mutexExperiment = ProjectConfig.GetExperimentFromKey("group_experiment_1");
-            DecisionServiceMock.Setup(ds => ds.GetVariation(It.IsAny<Experiment>(), It.IsAny<string>(), ProjectConfig, It.IsAny<UserAttributes>())).
+            DecisionServiceMock.Setup(ds => ds.GetVariation(It.IsAny<Experiment>(), It.IsAny<string>(), ProjectConfig, It.IsAny<UserAttributes>(), It.IsAny<List<OptimizelyDecideOption>>(), It.IsAny<IDecisionReasons>())).
                 Returns<Variation>(null);
 
             var featureFlag = ProjectConfig.GetFeatureFlagFromKey("boolean_feature");
-            var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", new UserAttributes(), ProjectConfig);
+            var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", new UserAttributes(), ProjectConfig, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance());
 
             Assert.IsNull(actualDecision);
 
@@ -582,7 +583,7 @@ public void TestGetVariationForFeatureRolloutWhenNoRuleInRollouts()
 
             var decisionService = new DecisionService(new Bucketer(new NoOpLogger()), new NoOpErrorHandler(), null, new NoOpLogger());
 
-            var variation = decisionService.GetVariationForFeatureRollout(featureFlag, "userId1", null, projectConfig);
+            var variation = decisionService.GetVariationForFeatureRollout(featureFlag, "userId1", null, projectConfig, DefaultDecisionReasons.NewInstance());
 
             Assert.IsNull(variation);
         }
@@ -600,9 +601,9 @@ public void TestGetVariationForFeatureRolloutWhenRolloutIsNotInDataFile()
                 Variables = featureFlag.Variables
             };
 
-            DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny<FeatureFlag>(), It.IsAny<string>(), It.IsAny<UserAttributes>(), ProjectConfig)).Returns<Variation>(null);
+            DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny<FeatureFlag>(), It.IsAny<string>(), It.IsAny<UserAttributes>(), ProjectConfig, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance())).Returns<Variation>(null);
 
-            var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureRollout(featureFlag, "user1", new UserAttributes(), ProjectConfig);
+            var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureRollout(featureFlag, "user1", new UserAttributes(), ProjectConfig, DefaultDecisionReasons.NewInstance());
             Assert.IsNull(actualDecision);
 
             LoggerMock.Verify(l => l.Log(LogLevel.INFO, "The feature flag \"boolean_feature\" is not used in a rollout."));
@@ -624,10 +625,10 @@ public void TestGetVariationForFeatureRolloutWhenUserIsBucketedInTheTargetingRul
             };
 
             BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(),
-                It.IsAny<string>())).Returns(variation);
+                It.IsAny<string>(), It.IsAny<IDecisionReasons>())).Returns(variation);
             var decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, null, LoggerMock.Object);
 
-            var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", userAttributes, ProjectConfig);
+            var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", userAttributes, ProjectConfig, DefaultDecisionReasons.NewInstance());
             Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision));
         }
 
@@ -648,11 +649,11 @@ public void TestGetVariationForFeatureRolloutWhenUserIsNotBucketedInTheTargeting
                 { "browser_type", "chrome" }
             };
 
-            BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), experiment, It.IsAny<string>(), It.IsAny<string>())).Returns<Variation>(null);
-            BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), everyoneElseRule, It.IsAny<string>(), It.IsAny<string>())).Returns(variation);
+            BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), experiment, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>())).Returns<Variation>(null);
+            BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), everyoneElseRule, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>())).Returns(variation);
             var decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, null, LoggerMock.Object);
 
-            var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", userAttributes, ProjectConfig);
+            var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", userAttributes, ProjectConfig, DefaultDecisionReasons.NewInstance());
             Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision));
         }
 
@@ -668,10 +669,10 @@ public void TestGetVariationForFeatureRolloutWhenUserIsNeitherBucketedInTheTarge
                 { "browser_type", "chrome" }
             };
 
-            BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>())).Returns<Variation>(null);
+            BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), It.IsAny<Experiment>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>())).Returns<Variation>(null);
             var decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, null, LoggerMock.Object);
 
-            var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", userAttributes, ProjectConfig);
+            var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", userAttributes, ProjectConfig, DefaultDecisionReasons.NewInstance());
             Assert.IsNull(actualDecision);
         }
 
@@ -689,11 +690,11 @@ public void TestGetVariationForFeatureRolloutWhenUserDoesNotQualifyForAnyTargeti
             var variation = everyoneElseRule.Variations[0];
             var expectedDecision = new FeatureDecision(everyoneElseRule, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT);
 
-            BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), everyoneElseRule, It.IsAny<string>(), It.IsAny<string>())).Returns(variation);
+            BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), everyoneElseRule, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>())).Returns(variation);
             var decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, null, LoggerMock.Object);
 
             // Provide null attributes so that user does not qualify for audience.
-            var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", null, ProjectConfig);
+            var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", null, ProjectConfig, DefaultDecisionReasons.NewInstance());
 
             Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision));
 
@@ -723,7 +724,7 @@ public void TestGetVariationForFeatureRolloutAudienceAndTrafficeAllocationCheck(
             {
                 { "device_type", "iPhone" },
                 { "location", "San Francisco" }
-            }, ProjectConfig);
+            }, ProjectConfig, DefaultDecisionReasons.NewInstance());
 
             // Returned variation id should be '177773' because of audience 'iPhone users in San Francisco'.
             var expectedDecision = new FeatureDecision(expWithAudienceiPhoneUsers, varWithAudienceiPhoneUsers, FeatureDecision.DECISION_SOURCE_ROLLOUT);
@@ -733,7 +734,7 @@ public void TestGetVariationForFeatureRolloutAudienceAndTrafficeAllocationCheck(
             actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, new UserAttributes
             {
                 { "browser_type", "chrome" }
-            }, ProjectConfig);
+            }, ProjectConfig, DefaultDecisionReasons.NewInstance());
 
             // Returned variation id should be '177771' because of audience 'Chrome users'.
             expectedDecision = new FeatureDecision(expWithAudienceChromeUsers, varWithAudienceChromeUsers, FeatureDecision.DECISION_SOURCE_ROLLOUT);
@@ -741,7 +742,7 @@ public void TestGetVariationForFeatureRolloutAudienceAndTrafficeAllocationCheck(
 
             // Calling with no audience.
             mockBucketer.Setup(bm => bm.GenerateBucketValue(It.IsAny<string>())).Returns(8000);
-            actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, new UserAttributes(), ProjectConfig);
+            actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, new UserAttributes(), ProjectConfig, DefaultDecisionReasons.NewInstance());
 
             // Returned variation id should be of everyone else rule because of no audience.
             expectedDecision = new FeatureDecision(expWithNoAudience, varWithNoAudience, FeatureDecision.DECISION_SOURCE_ROLLOUT);
@@ -752,7 +753,7 @@ public void TestGetVariationForFeatureRolloutAudienceAndTrafficeAllocationCheck(
             actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, new UserAttributes
             {
                 { "browser_type", "chrome" }
-            }, ProjectConfig);
+            }, ProjectConfig, DefaultDecisionReasons.NewInstance());
 
             // Returned decision entity should be null because bucket value exceeds traffice allocation of everyone else rule.
             Assert.Null(actualDecision);
@@ -768,21 +769,21 @@ public void TestGetVariationForFeatureRolloutCheckAudienceInEveryoneElseRule()
             var variation = everyoneElseRule.Variations[0];
             var expectedDecision = new FeatureDecision(everyoneElseRule, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT);
 
-            BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), everyoneElseRule, It.IsAny<string>(), WhitelistedUserId)).Returns(variation);
-            BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), everyoneElseRule, It.IsAny<string>(), GenericUserId)).Returns<Variation>(null);
+            BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), everyoneElseRule, It.IsAny<string>(), WhitelistedUserId, It.IsAny<IDecisionReasons>())).Returns(variation);
+            BucketerMock.Setup(bm => bm.Bucket(It.IsAny<ProjectConfig>(), everyoneElseRule, It.IsAny<string>(), GenericUserId, It.IsAny<IDecisionReasons>())).Returns<Variation>(null);
             var decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, null, LoggerMock.Object);
 
             // Returned variation id should be of everyone else rule as it passes audience Id checking.
-            var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, WhitelistedUserId, null, ProjectConfig);
+            var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, WhitelistedUserId, null, ProjectConfig, DefaultDecisionReasons.NewInstance());
             Assert.True(TestData.CompareObjects(expectedDecision, actualDecision));
 
             // Returned variation id should be null.
-            actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, null, ProjectConfig);
+            actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, null, ProjectConfig, DefaultDecisionReasons.NewInstance());
             Assert.Null(actualDecision);
 
             // Returned variation id should be null as it fails audience Id checking.
             everyoneElseRule.AudienceIds = new string[] { ProjectConfig.Audiences[0].Id };
-            actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, null, ProjectConfig);
+            actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, null, ProjectConfig, DefaultDecisionReasons.NewInstance());
             Assert.Null(actualDecision);
 
             LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "User \"testUser1\" does not meet the conditions for targeting rule \"1\"."), Times.Once);
@@ -807,7 +808,7 @@ public void TestGetVariationForFeatureWhenTheUserIsBucketedIntoFeatureExperiment
             var expectedDecision = new FeatureDecision(expectedExperiment, variation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST);
 
             DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny<FeatureFlag>(), It.IsAny<string>(),
-                It.IsAny<UserAttributes>(), ProjectConfig)).Returns(expectedDecision);
+                It.IsAny<UserAttributes>(), ProjectConfig, It.IsAny<List<OptimizelyDecideOption>>(), It.IsAny<IDecisionReasons>())).Returns(expectedDecision);
 
             var actualDecision = DecisionServiceMock.Object.GetVariationForFeature(featureFlag, "user1", ProjectConfig, new UserAttributes());
             Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision));
@@ -826,9 +827,9 @@ public void TestGetVariationForFeatureWhenTheUserIsNotBucketedIntoFeatureExperim
             var expectedDecision = new FeatureDecision(expectedExperiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT);
 
             DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny<FeatureFlag>(), It.IsAny<string>(),
-                It.IsAny<UserAttributes>(), ProjectConfig)).Returns<Variation>(null);
+                It.IsAny<UserAttributes>(), ProjectConfig, It.IsAny<List<OptimizelyDecideOption>>(), It.IsAny<IDecisionReasons>())).Returns<Variation>(null);
             DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureRollout(It.IsAny<FeatureFlag>(), It.IsAny<string>(),
-                It.IsAny<UserAttributes>(), ProjectConfig)).Returns(expectedDecision);
+                It.IsAny<UserAttributes>(), ProjectConfig, It.IsAny<IDecisionReasons>())).Returns(expectedDecision);
 
             var actualDecision = DecisionServiceMock.Object.GetVariationForFeature(featureFlag, "user1", ProjectConfig, new UserAttributes());
 
@@ -844,8 +845,8 @@ public void TestGetVariationForFeatureWhenTheUserIsNeitherBucketedIntoFeatureExp
             var featureFlag = ProjectConfig.GetFeatureFlagFromKey("string_single_variable_feature");
             var expectedDecision = new FeatureDecision(null, null, FeatureDecision.DECISION_SOURCE_ROLLOUT);
 
-            DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny<FeatureFlag>(), It.IsAny<string>(), It.IsAny<UserAttributes>(), ProjectConfig)).Returns<Variation>(null);
-            DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureRollout(It.IsAny<FeatureFlag>(), It.IsAny<string>(), It.IsAny<UserAttributes>(), ProjectConfig)).Returns<Variation>(null);
+            DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny<FeatureFlag>(), It.IsAny<string>(), It.IsAny<UserAttributes>(), ProjectConfig, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance())).Returns<Variation>(null);
+            DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureRollout(It.IsAny<FeatureFlag>(), It.IsAny<string>(), It.IsAny<UserAttributes>(), ProjectConfig, DefaultDecisionReasons.NewInstance())).Returns<Variation>(null);
 
             var actualDecision = DecisionServiceMock.Object.GetVariationForFeature(featureFlag, "user1", ProjectConfig, new UserAttributes());
             Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision));
@@ -866,8 +867,8 @@ public void TestGetVariationForFeatureWhenTheUserIsBuckedtedInBothExperimentAndR
                 { "browser_type", "chrome" }
             };
 
-            DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, "user1", ProjectConfig, userAttributes)).Returns(variation);
-            var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig);
+            DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, "user1", ProjectConfig, userAttributes, It.IsAny<List<OptimizelyDecideOption>>(), It.IsAny<IDecisionReasons>())).Returns(variation);
+            var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance());
 
             // The user is bucketed into feature experiment's variation.
             Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision));
@@ -877,8 +878,8 @@ public void TestGetVariationForFeatureWhenTheUserIsBuckedtedInBothExperimentAndR
             var rolloutVariation = rolloutExperiment.Variations[0];
             var expectedRolloutDecision = new FeatureDecision(rolloutExperiment, rolloutVariation, FeatureDecision.DECISION_SOURCE_ROLLOUT);
 
-            BucketerMock.Setup(bm => bm.Bucket(ProjectConfig, rolloutExperiment, It.IsAny<string>(), It.IsAny<string>())).Returns(rolloutVariation);
-            var actualRolloutDecision = DecisionServiceMock.Object.GetVariationForFeatureRollout(featureFlag, "user1", userAttributes, ProjectConfig);
+            BucketerMock.Setup(bm => bm.Bucket(ProjectConfig, rolloutExperiment, It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IDecisionReasons>())).Returns(rolloutVariation);
+            var actualRolloutDecision = DecisionServiceMock.Object.GetVariationForFeatureRollout(featureFlag, "user1", userAttributes, ProjectConfig, DefaultDecisionReasons.NewInstance());
 
             // The user is bucketed into feature rollout's variation.
 
diff --git a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs
new file mode 100644
index 00000000..029c8499
--- /dev/null
+++ b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs
@@ -0,0 +1,201 @@
+/**
+ *
+ *    Copyright 2020, Optimizely and contributors
+ *
+ *    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 Castle.Core.Internal;
+using Moq;
+using NUnit.Framework;
+using OptimizelySDK.Entity;
+using OptimizelySDK.ErrorHandler;
+using OptimizelySDK.Event.Dispatcher;
+using OptimizelySDK.Logger;
+using OptimizelySDK.OptimizelyDecisions;
+using System;
+using System.Collections.Generic;
+
+namespace OptimizelySDK.Tests
+{
+    [TestFixture]
+    public class OptimizelyUserContextTest
+    {
+        string UserID = "testUserID";
+        private Optimizely Optimizely;
+        private Mock<ILogger> LoggerMock;
+        private Mock<IErrorHandler> ErrorHandlerMock;
+        private Mock<IEventDispatcher> EventDispatcherMock;
+
+        [SetUp]
+        public void SetUp()
+        {
+            LoggerMock = new Mock<ILogger>();
+            LoggerMock.Setup(i => i.Log(It.IsAny<LogLevel>(), It.IsAny<string>()));
+
+            ErrorHandlerMock = new Mock<IErrorHandler>();
+            ErrorHandlerMock.Setup(e => e.HandleError(It.IsAny<Exception>()));
+            EventDispatcherMock = new Mock<IEventDispatcher>();
+
+            Optimizely = new Optimizely(TestData.Datafile, EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object);
+        }
+
+        [Test]
+        public void OptimizelyUserContext_withAttributes()
+        {
+            var attributes = new UserAttributes() { { "house", "GRYFFINDOR" } };
+            OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object);
+
+            Assert.AreEqual(user.Optimizely, Optimizely);
+            Assert.AreEqual(user.UserId, UserID);
+            Assert.AreEqual(user.UserAttributes, attributes);
+        }
+
+        [Test]
+        public void OptimizelyUserContext_noAttributes()
+        {
+            OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, null, ErrorHandlerMock.Object, LoggerMock.Object);
+
+            Assert.AreEqual(user.Optimizely, Optimizely);
+            Assert.AreEqual(user.UserId, UserID);
+            Assert.True(user.UserAttributes.Count == 0);
+        }
+
+        [Test]
+        public void SetAttribute()
+        {
+            var attributes = new UserAttributes() { { "house", "GRYFFINDOR" } };
+            OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object);
+
+            user.SetAttribute("k1", "v1");
+            user.SetAttribute("k2", true);
+            user.SetAttribute("k3", 100);
+            user.SetAttribute("k4", 3.5);
+
+            Assert.AreEqual(user.Optimizely, Optimizely);
+            Assert.AreEqual(user.UserId, UserID);
+            var newAttributes = user.UserAttributes;
+            Assert.AreEqual(newAttributes["house"], "GRYFFINDOR");
+            Assert.AreEqual(newAttributes["k1"], "v1");
+            Assert.AreEqual(newAttributes["k2"], true);
+            Assert.AreEqual(newAttributes["k3"], 100);
+            Assert.AreEqual(newAttributes["k4"], 3.5);
+        }
+
+        [Test]
+        public void SetAttribute_noAttribute()
+        {
+            OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, null, ErrorHandlerMock.Object, LoggerMock.Object);
+
+            user.SetAttribute("k1", "v1");
+            user.SetAttribute("k2", true);
+
+            Assert.AreEqual(user.Optimizely, Optimizely);
+            Assert.AreEqual(user.UserId, UserID);
+            var newAttributes = user.UserAttributes;
+            Assert.AreEqual(newAttributes["k1"], "v1");
+            Assert.AreEqual(newAttributes["k2"], true);
+        }
+
+        [Test]
+        public void SetAttribute_override()
+        {
+            var attributes = new UserAttributes() { { "house", "GRYFFINDOR" } };
+            OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object);
+
+            user.SetAttribute("k1", "v1");
+            user.SetAttribute("house", "v2");
+
+            var newAttributes = user.UserAttributes;
+            Assert.AreEqual(newAttributes["k1"], "v1");
+            Assert.AreEqual(newAttributes["house"], "v2");
+        }
+
+        [Test]
+        public void SetAttribute_nullValue()
+        {
+            var attributes = new UserAttributes() { { "k1", null } };
+            OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object);
+
+            var newAttributes = user.UserAttributes;
+            Assert.AreEqual(newAttributes["k1"], null);
+
+            user.SetAttribute("k1", true);
+            newAttributes = user.UserAttributes;
+            Assert.AreEqual(newAttributes["k1"], true);
+
+            user.SetAttribute("k1", null);
+            newAttributes = user.UserAttributes;
+            Assert.AreEqual(newAttributes["k1"], null);
+        }
+
+        #region decide
+
+        [Test]
+        public void Decide()
+        {
+            var flagKey = "multi_variate_feature";
+            var variablesExpected = Optimizely.GetAllFeatureVariables(flagKey, UserID);
+
+            var user = Optimizely.CreateUserContext(UserID);
+            user.SetAttribute("browser_type", "chrome");
+            var decision = user.Decide(flagKey);
+
+            Assert.AreEqual(decision.VariationKey, "Gred");
+            Assert.False(decision.Enabled);
+            Assert.AreEqual(decision.Variables.ToDictionary(), variablesExpected.ToDictionary());
+            Assert.AreEqual(decision.RuleKey, "test_experiment_multivariate");
+            Assert.AreEqual(decision.FlagKey, flagKey);
+            Assert.AreEqual(decision.UserContext, user);
+            Assert.True(decision.Reasons.IsNullOrEmpty());
+        }
+
+        [Test]
+        public void DecideInvalidFlagKey()
+        {
+            var flagKey = "invalid_feature";
+
+            var user = Optimizely.CreateUserContext(UserID);
+            user.SetAttribute("browser_type", "chrome");
+            var decision = user.Decide(flagKey);
+
+            Assert.Null(decision.VariationKey);
+            Assert.False(decision.Enabled);
+            Assert.AreEqual(decision.Variables.ToDictionary(), new Dictionary<string, object>());
+            Assert.IsNull(decision.RuleKey);
+            Assert.AreEqual(decision.FlagKey, flagKey);
+            Assert.AreEqual(decision.UserContext, user);
+            Assert.True(decision.Reasons.IsNullOrEmpty());
+        }
+
+        [Test]
+        public void DecideInvalidConfig()
+        {
+            Optimizely optimizely = new Optimizely(TestData.UnsupportedVersionDatafile, EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object);
+
+            var flagKey = "multi_variate_feature";
+            var decisionExpected = OptimizelyDecision.NewErrorDecision(
+                flagKey,
+                new OptimizelyUserContext(optimizely, UserID, new UserAttributes(), ErrorHandlerMock.Object, LoggerMock.Object),
+                DecisionMessage.SDK_NOT_READY,
+                ErrorHandlerMock.Object,
+                LoggerMock.Object);
+            var user = optimizely.CreateUserContext(UserID);
+            var decision = user.Decide(flagKey);
+
+            Assert.IsTrue(TestData.CompareObjects(decision, decisionExpected));
+        }
+        #endregion
+
+    }
+}
diff --git a/OptimizelySDK/Bucketing/Bucketer.cs b/OptimizelySDK/Bucketing/Bucketer.cs
index b8944687..be62727a 100644
--- a/OptimizelySDK/Bucketing/Bucketer.cs
+++ b/OptimizelySDK/Bucketing/Bucketer.cs
@@ -15,6 +15,7 @@
  */
 using OptimizelySDK.Entity;
 using OptimizelySDK.Logger;
+using OptimizelySDK.OptimizelyDecisions;
 using System;
 using System.Collections.Generic;
 using System.Text;
@@ -102,8 +103,9 @@ private string FindBucket(string bucketingId, string userId, string parentId, IE
         /// <param name="experiment">Experiment Experiment in which user is to be bucketed</param>
         /// <param name="bucketingId">A customer-assigned value used to create the key for the murmur hash.</param>
         /// <param name="userId">User identifier</param>
+        /// <param name="reasons">Decision log messages.</param>
         /// <returns>Variation which will be shown to the user</returns>
-        public virtual Variation Bucket(ProjectConfig config, Experiment experiment, string bucketingId, string userId)
+        public virtual Variation Bucket(ProjectConfig config, Experiment experiment, string bucketingId, string userId, IDecisionReasons reasons)
         {
             string message;
             Variation variation;
@@ -122,26 +124,26 @@ public virtual Variation Bucket(ProjectConfig config, Experiment experiment, str
                 if (string.IsNullOrEmpty(userExperimentId))
                 {
                     message = $"User [{userId}] is in no experiment.";
-                    Logger.Log(LogLevel.INFO, message);
+                    Logger.Log(LogLevel.INFO, reasons.AddInfo(message));
                     return new Variation();
                 }
 
                 if (userExperimentId != experiment.Id)
                 {
                     message = $"User [{userId}] is not in experiment [{experiment.Key}] of group [{experiment.GroupId}].";
-                    Logger.Log(LogLevel.INFO, message);
+                    Logger.Log(LogLevel.INFO, reasons.AddInfo(message));
                     return new Variation();
                 }
 
                 message = $"User [{userId}] is in experiment [{experiment.Key}] of group [{experiment.GroupId}].";
-                Logger.Log(LogLevel.INFO, message);
+                Logger.Log(LogLevel.INFO, reasons.AddInfo(message));
             }
 
             // Bucket user if not in whitelist and in group (if any).
             string variationId = FindBucket(bucketingId, userId, experiment.Id, experiment.TrafficAllocation);
             if (string.IsNullOrEmpty(variationId))
             {
-                Logger.Log(LogLevel.INFO, $"User [{userId}] is in no variation.");
+                Logger.Log(LogLevel.INFO, reasons.AddInfo($"User [{userId}] is in no variation."));
                 return new Variation();
 
             }
@@ -149,7 +151,7 @@ public virtual Variation Bucket(ProjectConfig config, Experiment experiment, str
             // success!
             variation = config.GetVariationFromId(experiment.Key, variationId);
             message = $"User [{userId}] is in variation [{variation.Key}] of experiment [{experiment.Key}].";
-            Logger.Log(LogLevel.INFO, message);
+            Logger.Log(LogLevel.INFO, reasons.AddInfo(message));
             return variation;
         }
     }
diff --git a/OptimizelySDK/Bucketing/DecisionService.cs b/OptimizelySDK/Bucketing/DecisionService.cs
index 3959dbbd..3973b13e 100644
--- a/OptimizelySDK/Bucketing/DecisionService.cs
+++ b/OptimizelySDK/Bucketing/DecisionService.cs
@@ -19,6 +19,7 @@
 using OptimizelySDK.Entity;
 using OptimizelySDK.ErrorHandler;
 using OptimizelySDK.Logger;
+using OptimizelySDK.OptimizelyDecisions;
 using OptimizelySDK.Utils;
 
 namespace OptimizelySDK.Bucketing
@@ -83,20 +84,43 @@ public DecisionService(Bucketer bucketer, IErrorHandler errorHandler, UserProfil
         /// <param name = "userId" > The userId of the user.
         /// <param name = "filteredAttributes" > The user's attributes. This should be filtered to just attributes in the Datafile.</param>
         /// <returns>The Variation the user is allocated into.</returns>
-        public virtual Variation GetVariation(Experiment experiment, string userId, ProjectConfig config, UserAttributes filteredAttributes)
+        public virtual Variation GetVariation(Experiment experiment,
+            string userId,
+            ProjectConfig config,
+            UserAttributes filteredAttributes)
+        {
+            return GetVariation(experiment, userId, config, filteredAttributes, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance());
+        }
+
+        /// <summary>
+        /// Get a Variation of an Experiment for a user to be allocated into.
+        /// </summary>
+        /// <param name = "experiment" > The Experiment the user will be bucketed into.</param>
+        /// <param name = "userId" > The userId of the user.
+        /// <param name = "filteredAttributes" > The user's attributes. This should be filtered to just attributes in the Datafile.</param>
+        /// <returns>The Variation the user is allocated into.</returns>
+        public virtual Variation GetVariation(Experiment experiment,
+            string userId,
+            ProjectConfig config,
+            UserAttributes filteredAttributes,
+            List<OptimizelyDecideOption> options,
+            IDecisionReasons reasons)
         {
             if (!ExperimentUtils.IsExperimentActive(experiment, Logger)) return null;
 
             // check if a forced variation is set
-            var forcedVariation = GetForcedVariation(experiment.Key, userId, config);
+            var forcedVariation = GetForcedVariation(experiment.Key, userId, config, reasons);
             if (forcedVariation != null)
                 return forcedVariation;
 
-            var variation = GetWhitelistedVariation(experiment, userId);
+            var variation = GetWhitelistedVariation(experiment, userId, reasons);
 
             if (variation != null) return variation;
+            // fetch the user profile map from the user profile service
+            var ignoreUPS = options.Contains(OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE);
+
             UserProfile userProfile = null;
-            if (UserProfileService != null)
+            if (UserProfileService != null && !ignoreUPS)
             {
                 try
                 {
@@ -104,21 +128,21 @@ public virtual Variation GetVariation(Experiment experiment, string userId, Proj
                     if (userProfileMap != null && UserProfileUtil.IsValidUserProfileMap(userProfileMap))
                     {
                         userProfile = UserProfileUtil.ConvertMapToUserProfile(userProfileMap);
-                        variation = GetStoredVariation(experiment, userProfile, config);
+                        variation = GetStoredVariation(experiment, userProfile, config, reasons);
                         if (variation != null) return variation;
                     }
                     else if (userProfileMap == null)
                     {
-                        Logger.Log(LogLevel.INFO, "We were unable to get a user profile map from the UserProfileService.");
+                        Logger.Log(LogLevel.INFO, reasons.AddInfo("We were unable to get a user profile map from the UserProfileService."));
                     }
                     else
                     {
-                        Logger.Log(LogLevel.ERROR, "The UserProfileService returned an invalid map.");
+                        Logger.Log(LogLevel.ERROR, reasons.AddInfo("The UserProfileService returned an invalid map."));
                     }
                 }
                 catch (Exception exception)
                 {
-                    Logger.Log(LogLevel.ERROR, exception.Message);
+                    Logger.Log(LogLevel.ERROR, reasons.AddInfo(exception.Message));
                     ErrorHandler.HandleError(new Exceptions.OptimizelyRuntimeException(exception.Message));
                 }
             }
@@ -126,16 +150,16 @@ public virtual Variation GetVariation(Experiment experiment, string userId, Proj
             if (ExperimentUtils.DoesUserMeetAudienceConditions(config, experiment, filteredAttributes, LOGGING_KEY_TYPE_EXPERIMENT, experiment.Key, Logger))
             {
                 // Get Bucketing ID from user attributes.
-                string bucketingId = GetBucketingId(userId, filteredAttributes);
+                string bucketingId = GetBucketingId(userId, filteredAttributes, reasons);
 
-                variation = Bucketer.Bucket(config, experiment, bucketingId, userId);
+                variation = Bucketer.Bucket(config, experiment, bucketingId, userId, reasons);
 
                 if (variation != null && variation.Key != null)
                 {
                     if (UserProfileService != null)
                     {
                         var bucketerUserProfile = userProfile ?? new UserProfile(userId, new Dictionary<string, Decision>());
-                        SaveVariation(experiment, variation, bucketerUserProfile);
+                        SaveVariation(experiment, variation, bucketerUserProfile, reasons);
 
                     }
                     else
@@ -144,7 +168,7 @@ public virtual Variation GetVariation(Experiment experiment, string userId, Proj
 
                 return variation;
             }
-            Logger.Log(LogLevel.INFO, $"User \"{userId}\" does not meet conditions to be in experiment \"{experiment.Key}\".");
+            Logger.Log(LogLevel.INFO, reasons.AddInfo($"User \"{userId}\" does not meet conditions to be in experiment \"{experiment.Key}\"."));
 
             return null;
         }
@@ -157,6 +181,18 @@ public virtual Variation GetVariation(Experiment experiment, string userId, Proj
         /// <param name="config">Project Config</param>
         /// <returns>Variation entity which the given user and experiment should be forced into.</returns>
         public Variation GetForcedVariation(string experimentKey, string userId, ProjectConfig config)
+        {
+            return GetForcedVariation(experimentKey, userId, config, DefaultDecisionReasons.NewInstance());
+        }
+
+        /// <summary>
+        /// Gets the forced variation for the given user and experiment.  
+        /// </summary>
+        /// <param name="experimentKey">The experiment key</param>
+        /// <param name="userId">The user ID</param>
+        /// <param name="config">Project Config</param>
+        /// <returns>Variation entity which the given user and experiment should be forced into.</returns>
+        public Variation GetForcedVariation(string experimentKey, string userId, ProjectConfig config, IDecisionReasons reasons)
         {
             if (ForcedVariationMap.ContainsKey(userId) == false)
             {
@@ -191,8 +227,7 @@ public Variation GetForcedVariation(string experimentKey, string userId, Project
             // this case is logged in getVariationFromKey   
             if (string.IsNullOrEmpty(variationKey))
                 return null;
-
-            Logger.Log(LogLevel.DEBUG, $@"Variation ""{variationKey}"" is mapped to experiment ""{experimentKey}"" and user ""{userId}"" in the forced variation map");
+            Logger.Log(LogLevel.DEBUG, reasons.AddInfo($@"Variation ""{variationKey}"" is mapped to experiment ""{experimentKey}"" and user ""{userId}"" in the forced variation map"));
 
             Variation variation = config.GetVariationFromKey(experimentKey, variationKey);
 
@@ -248,8 +283,6 @@ public bool SetForcedVariation(string experimentKey, string userId, string varia
             Logger.Log(LogLevel.DEBUG, $@"Set variation ""{variationId}"" for experiment ""{experimentId}"" and user ""{userId}"" in the forced variation map.");
             return true;
         }
-
-
         /// <summary>
         /// Get the variation the user has been whitelisted into.
         /// </summary>
@@ -258,6 +291,19 @@ public bool SetForcedVariation(string experimentKey, string userId, string varia
         /// <returns>if the user is not whitelisted into any variation {@link Variation}
         /// the user is bucketed into if the user has a specified whitelisted variation.</returns>
         public Variation GetWhitelistedVariation(Experiment experiment, string userId)
+        {
+            return GetWhitelistedVariation(experiment, userId, DefaultDecisionReasons.NewInstance());
+        }
+
+        /// <summary>
+        /// Get the variation the user has been whitelisted into.
+        /// </summary>
+        /// <param name = "experiment" >in which user is to be bucketed.</param>
+        /// <param name = "userId" > User Identifier</param>
+        /// <param name = "reasons" > Decision log messages.</param>
+        /// <returns>if the user is not whitelisted into any variation {@link Variation}
+        /// the user is bucketed into if the user has a specified whitelisted variation.</returns>
+        public Variation GetWhitelistedVariation(Experiment experiment, string userId, IDecisionReasons reasons)
         {
             //if a user has a forced variation mapping, return the respective variation
             Dictionary<string, string> userIdToVariationKeyMap = experiment.UserIdToKeyVariations;
@@ -271,9 +317,9 @@ public Variation GetWhitelistedVariation(Experiment experiment, string userId)
                 : null;
 
             if (forcedVariation != null)
-                Logger.Log(LogLevel.INFO, $"User \"{userId}\" is forced in variation \"{forcedVariationKey}\".");
+                Logger.Log(LogLevel.INFO, reasons.AddInfo($"User \"{userId}\" is forced in variation \"{forcedVariationKey}\"."));
             else
-                Logger.Log(LogLevel.ERROR, $"Variation \"{forcedVariationKey}\" is not in the datafile. Not activating user \"{userId}\".");
+                Logger.Log(LogLevel.ERROR, reasons.AddInfo($"Variation \"{forcedVariationKey}\" is not in the datafile. Not activating user \"{userId}\"."));
 
             return forcedVariation;
         }
@@ -284,7 +330,7 @@ public Variation GetWhitelistedVariation(Experiment experiment, string userId)
         /// <param name = "experiment" > which the user was bucketed</param>
         /// <param name = "userProfile" > User profile of the user</param>
         /// <returns>The user was previously bucketed into.</returns>
-        public Variation GetStoredVariation(Experiment experiment, UserProfile userProfile, ProjectConfig config)
+        public Variation GetStoredVariation(Experiment experiment, UserProfile userProfile, ProjectConfig config, IDecisionReasons reasons)
         {
             // ---------- Check User Profile for Sticky Bucketing ----------
             // If a user profile instance is present then check it for a saved variation
@@ -296,7 +342,7 @@ public Variation GetStoredVariation(Experiment experiment, UserProfile userProfi
 
             if (decision == null)
             {
-                Logger.Log(LogLevel.INFO, $"No previously activated variation of experiment \"{experimentKey}\" for user \"{userProfile.UserId}\" found in user profile.");
+                Logger.Log(LogLevel.INFO, reasons.AddInfo($"No previously activated variation of experiment \"{experimentKey}\" for user \"{userProfile.UserId}\" found in user profile."));
                 return null;
             }
 
@@ -310,11 +356,11 @@ public Variation GetStoredVariation(Experiment experiment, UserProfile userProfi
 
                 if (savedVariation == null)
                 {
-                    Logger.Log(LogLevel.INFO, $"User \"{userProfile.UserId}\" was previously bucketed into variation with ID \"{variationId}\" for experiment \"{experimentId}\", but no matching variation was found for that user. We will re-bucket the user.");
+                    Logger.Log(LogLevel.INFO, reasons.AddInfo($"User \"{userProfile.UserId}\" was previously bucketed into variation with ID \"{variationId}\" for experiment \"{experimentId}\", but no matching variation was found for that user. We will re-bucket the user."));
                     return null;
                 }
 
-                Logger.Log(LogLevel.INFO, $"Returning previously activated variation \"{savedVariation.Key}\" of experiment \"{experimentKey}\" for user \"{userProfile.UserId}\" from user profile.");
+                Logger.Log(LogLevel.INFO, reasons.AddInfo($"Returning previously activated variation \"{savedVariation.Key}\" of experiment \"{experimentKey}\" for user \"{userProfile.UserId}\" from user profile."));
                 return savedVariation;
             }
             catch (Exception)
@@ -322,7 +368,6 @@ public Variation GetStoredVariation(Experiment experiment, UserProfile userProfi
                 return null;
             }
         }
-
         /// <summary>
         /// Save a { @link Variation } of an { @link Experiment } for a user in the {@link UserProfileService}.
         /// </summary>
@@ -330,6 +375,17 @@ public Variation GetStoredVariation(Experiment experiment, UserProfile userProfi
         /// <param name = "variation" > The Variation to save.</param>
         /// <param name = "userProfile" > instance of the user information.</param>
         public void SaveVariation(Experiment experiment, Variation variation, UserProfile userProfile)
+        {
+            SaveVariation(experiment, variation, userProfile, DefaultDecisionReasons.NewInstance());
+        }
+
+        /// <summary>
+        /// Save a { @link Variation } of an { @link Experiment } for a user in the {@link UserProfileService}.
+        /// </summary>
+        /// <param name = "experiment" > The experiment the user was buck</param>
+        /// <param name = "variation" > The Variation to save.</param>
+        /// <param name = "userProfile" > instance of the user information.</param>
+        public void SaveVariation(Experiment experiment, Variation variation, UserProfile userProfile, IDecisionReasons reasons)
         {
             //only save if the user has implemented a user profile service
             if (UserProfileService == null)
@@ -351,15 +407,15 @@ public void SaveVariation(Experiment experiment, Variation variation, UserProfil
             try
             {
                 UserProfileService.Save(userProfile.ToMap());
-                Logger.Log(LogLevel.INFO, $"Saved variation \"{variation.Id}\" of experiment \"{experiment.Id}\" for user \"{userProfile.UserId}\".");
+                Logger.Log(LogLevel.INFO, reasons.AddInfo($"Saved variation \"{variation.Id}\" of experiment \"{experiment.Id}\" for user \"{userProfile.UserId}\"."));
             }
             catch (Exception exception)
             {
-                Logger.Log(LogLevel.ERROR, $"Failed to save variation \"{variation.Id}\" of experiment \"{experiment.Id}\" for user \"{userProfile.UserId}\".");
+                Logger.Log(LogLevel.ERROR, reasons.AddInfo($"Failed to save variation \"{variation.Id}\" of experiment \"{experiment.Id}\" for user \"{userProfile.UserId}\"."));
                 ErrorHandler.HandleError(new Exceptions.OptimizelyRuntimeException(exception.Message));
             }
         }
-
+       
         /// <summary>
         /// Try to bucket the user into a rollout rule.
         /// Evaluate the user for rules in priority order by seeing if the user satisfies the audience.
@@ -368,9 +424,14 @@ public void SaveVariation(Experiment experiment, Variation variation, UserProfil
         /// <param name = "featureFlag" >The feature flag the user wants to access.</param>
         /// <param name = "userId" >User Identifier</param>
         /// <param name = "filteredAttributes" >The user's attributes. This should be filtered to just attributes in the Datafile.</param>
+        /// <param name = "reasons" >Decision log messages.</param>
         /// <returns>null if the user is not bucketed into the rollout or if the feature flag was not attached to a rollout.
         /// otherwise the FeatureDecision entity</returns>
-        public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag featureFlag, string userId, UserAttributes filteredAttributes, ProjectConfig config)
+        public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag featureFlag,
+            string userId,
+            UserAttributes filteredAttributes,
+            ProjectConfig config,
+            IDecisionReasons reasons)
         {
             if (featureFlag == null)
             {
@@ -380,7 +441,7 @@ public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag feature
 
             if (string.IsNullOrEmpty(featureFlag.RolloutId))
             {
-                Logger.Log(LogLevel.INFO, $"The feature flag \"{featureFlag.Key}\" is not used in a rollout.");
+                Logger.Log(LogLevel.INFO, reasons.AddInfo($"The feature flag \"{featureFlag.Key}\" is not used in a rollout."));
                 return null;
             }
 
@@ -388,7 +449,7 @@ public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag feature
 
             if (string.IsNullOrEmpty(rollout.Id))
             {
-                Logger.Log(LogLevel.ERROR, $"The rollout with id \"{featureFlag.RolloutId}\" is not found in the datafile for feature flag \"{featureFlag.Key}\"");
+                Logger.Log(LogLevel.ERROR, reasons.AddInfo($"The rollout with id \"{featureFlag.RolloutId}\" is not found in the datafile for feature flag \"{featureFlag.Key}\""));
                 return null;
             }
 
@@ -400,16 +461,16 @@ public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag feature
             var rolloutRulesLength = rollout.Experiments.Count;
 
             // Get Bucketing ID from user attributes.
-            string bucketingId = GetBucketingId(userId, filteredAttributes);
+            string bucketingId = GetBucketingId(userId, filteredAttributes, reasons);
 
             // For all rules before the everyone else rule
             for (int i = 0; i < rolloutRulesLength - 1; i++)
             {
                 string loggingKey = (i + 1).ToString(); 
                 var rolloutRule = rollout.Experiments[i];
-                if (ExperimentUtils.DoesUserMeetAudienceConditions(config, rolloutRule, filteredAttributes, LOGGING_KEY_TYPE_RULE, loggingKey, Logger))
+                if (ExperimentUtils.DoesUserMeetAudienceConditions(config, rolloutRule, filteredAttributes, LOGGING_KEY_TYPE_RULE, loggingKey, reasons, Logger))
                 {
-                    variation = Bucketer.Bucket(config, rolloutRule, bucketingId, userId);
+                    variation = Bucketer.Bucket(config, rolloutRule, bucketingId, userId, reasons);
                     if (variation == null || string.IsNullOrEmpty(variation.Id))
                         break;
 
@@ -424,9 +485,9 @@ public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag feature
 
             // Get the last rule which is everyone else rule.
             var everyoneElseRolloutRule = rollout.Experiments[rolloutRulesLength - 1];
-            if (ExperimentUtils.DoesUserMeetAudienceConditions(config, everyoneElseRolloutRule, filteredAttributes, LOGGING_KEY_TYPE_RULE, "Everyone Else", Logger))
+            if (ExperimentUtils.DoesUserMeetAudienceConditions(config, everyoneElseRolloutRule, filteredAttributes, LOGGING_KEY_TYPE_RULE, "Everyone Else", reasons, Logger))
             {
-                variation = Bucketer.Bucket(config, everyoneElseRolloutRule, bucketingId, userId);
+                variation = Bucketer.Bucket(config, everyoneElseRolloutRule, bucketingId, userId, reasons);
                 if (variation != null && !string.IsNullOrEmpty(variation.Id))
                 {
                     Logger.Log(LogLevel.DEBUG, $"User \"{userId}\" meets conditions for targeting rule \"Everyone Else\".");
@@ -450,7 +511,12 @@ public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag feature
         /// <param name = "filteredAttributes" >The user's attributes. This should be filtered to just attributes in the Datafile.</param>
         /// <returns>null if the user is not bucketed into the rollout or if the feature flag was not attached to a rollout.
         /// Otherwise the FeatureDecision entity</returns>
-        public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag featureFlag, string userId, UserAttributes filteredAttributes, ProjectConfig config)
+        public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag featureFlag,
+            string userId,
+            UserAttributes filteredAttributes,
+            ProjectConfig config,
+            List<OptimizelyDecideOption> options,
+            IDecisionReasons reasons)
         {
             if (featureFlag == null)
             {
@@ -460,7 +526,7 @@ public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag feat
 
             if (featureFlag.ExperimentIds == null || featureFlag.ExperimentIds.Count == 0)
             {
-                Logger.Log(LogLevel.INFO, $"The feature flag \"{featureFlag.Key}\" is not used in any experiments.");
+                Logger.Log(LogLevel.INFO, reasons.AddInfo($"The feature flag \"{featureFlag.Key}\" is not used in any experiments."));
                 return null;
             }
 
@@ -471,16 +537,16 @@ public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag feat
                 if (string.IsNullOrEmpty(experiment.Key))
                     continue;
 
-                var variation = GetVariation(experiment, userId, config, filteredAttributes);
+                var variation = GetVariation(experiment, userId, config, filteredAttributes, options, reasons);
 
                 if (variation != null && !string.IsNullOrEmpty(variation.Id))
                 {
-                    Logger.Log(LogLevel.INFO, $"The user \"{userId}\" is bucketed into experiment \"{experiment.Key}\" of feature \"{featureFlag.Key}\".");
+                    Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is bucketed into experiment \"{experiment.Key}\" of feature \"{featureFlag.Key}\"."));
                     return new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST);
                 }
             }
 
-            Logger.Log(LogLevel.INFO, $"The user \"{userId}\" is not bucketed into any of the experiments on the feature \"{featureFlag.Key}\".");
+            Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is not bucketed into any of the experiments on the feature \"{featureFlag.Key}\"."));
             return null;
         }
 
@@ -493,23 +559,44 @@ public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag feat
         /// <returns>null if the user is not bucketed into any variation or the FeatureDecision entity if the user is 
         /// successfully bucketed.</returns>
         public virtual FeatureDecision GetVariationForFeature(FeatureFlag featureFlag, string userId, ProjectConfig config, UserAttributes filteredAttributes)
+        {
+            return GetVariationForFeature(featureFlag, userId, config, filteredAttributes, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance());
+        }
+
+        /// <summary>
+        /// Get the variation the user is bucketed into for the FeatureFlag
+        /// </summary>
+        /// <param name = "featureFlag" >The feature flag the user wants to access.</param>
+        /// <param name = "userId" >User Identifier</param>
+        /// <param name = "filteredAttributes" >The user's attributes. This should be filtered to just attributes in the Datafile.</param>
+        /// <param name = "filteredAttributes" >The user's attributes. This should be filtered to just attributes in the Datafile.</param>
+        /// <param name = "options" >An array of decision options.</param>
+        /// <param name = "reasons" >Decision log messages.</param>
+        /// <returns>null if the user is not bucketed into any variation or the FeatureDecision entity if the user is 
+        /// successfully bucketed.</returns>
+        public virtual FeatureDecision GetVariationForFeature(FeatureFlag featureFlag,
+            string userId,
+            ProjectConfig config,
+            UserAttributes filteredAttributes,
+            List<OptimizelyDecideOption> options, 
+            IDecisionReasons reasons)
         {
             // Check if the feature flag has an experiment and the user is bucketed into that experiment.
-            var decision = GetVariationForFeatureExperiment(featureFlag, userId, filteredAttributes, config);
+            var decision = GetVariationForFeatureExperiment(featureFlag, userId, filteredAttributes, config, options, reasons);
 
             if (decision != null)
                 return decision;
 
             // Check if the feature flag has rollout and the the user is bucketed into one of its rules.
-            decision = GetVariationForFeatureRollout(featureFlag, userId, filteredAttributes, config);
+            decision = GetVariationForFeatureRollout(featureFlag, userId, filteredAttributes, config, reasons);
 
             if (decision != null)
             {
-                Logger.Log(LogLevel.INFO, $"The user \"{userId}\" is bucketed into a rollout for feature flag \"{featureFlag.Key}\".");
+                Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is bucketed into a rollout for feature flag \"{featureFlag.Key}\"."));
                 return decision;
             }
 
-            Logger.Log(LogLevel.INFO, $"The user \"{userId}\" is not bucketed into a rollout for feature flag \"{featureFlag.Key}\".");
+            Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is not bucketed into a rollout for feature flag \"{featureFlag.Key}\"."));
             return new FeatureDecision(null, null, FeatureDecision.DECISION_SOURCE_ROLLOUT);
         }
 
@@ -519,7 +606,7 @@ public virtual FeatureDecision GetVariationForFeature(FeatureFlag featureFlag, s
         /// <param name = "userId" >User Identifier</param>
         /// <param name = "filteredAttributes" >The user's attributes.</param>
         /// <returns>Bucketing Id if it is a string type in attributes, user Id otherwise.</returns>
-        private string GetBucketingId(string userId, UserAttributes filteredAttributes)
+        private string GetBucketingId(string userId, UserAttributes filteredAttributes, IDecisionReasons reasons)
         {
             string bucketingId = userId;
 
@@ -533,7 +620,7 @@ private string GetBucketingId(string userId, UserAttributes filteredAttributes)
                 }
                 else
                 {
-                    Logger.Log(LogLevel.WARN, "BucketingID attribute is not a string. Defaulted to userId");
+                    Logger.Log(LogLevel.WARN, reasons.AddInfo("BucketingID attribute is not a string. Defaulted to userId"));
                 }
             }
 
diff --git a/OptimizelySDK/Optimizely.cs b/OptimizelySDK/Optimizely.cs
index b30d00ac..e64b38fe 100644
--- a/OptimizelySDK/Optimizely.cs
+++ b/OptimizelySDK/Optimizely.cs
@@ -476,45 +476,45 @@ public virtual bool IsFeatureEnabled(string featureKey, string userId, UserAttri
                 return false;
 
             bool featureEnabled = false;
-            var sourceInfo = new Dictionary<string, string>();
-            var decision = DecisionService.GetVariationForFeature(featureFlag, userId, config, userAttributes);
-            var variation = decision.Variation;
-            var decisionSource = decision?.Source ?? FeatureDecision.DECISION_SOURCE_ROLLOUT;
-
-            SendImpressionEvent(decision.Experiment, variation, userId, userAttributes, config, featureKey, decisionSource);
+                var sourceInfo = new Dictionary<string, string>();
+                var decision = DecisionService.GetVariationForFeature(featureFlag, userId, config, userAttributes);
+                var variation = decision.Variation;
+                var decisionSource = decision?.Source ?? FeatureDecision.DECISION_SOURCE_ROLLOUT;
 
-            if (variation != null)
-            {
-                featureEnabled = variation.FeatureEnabled.GetValueOrDefault();
+                SendImpressionEvent(decision.Experiment, variation, userId, userAttributes, config, featureKey, decisionSource);
 
-                // This information is only necessary for feature tests.
-                // For rollouts experiments and variations are an implementation detail only.
-                if (decision.Source == FeatureDecision.DECISION_SOURCE_FEATURE_TEST)
-                {
-                    sourceInfo["experimentKey"] = decision.Experiment.Key;
-                    sourceInfo["variationKey"] = variation.Key;
-                }
-                else
+                if (variation != null)
                 {
-                    Logger.Log(LogLevel.INFO, $@"The user ""{userId}"" is not being experimented on feature ""{featureKey}"".");
-                }
-            }
+                    featureEnabled = variation.FeatureEnabled.GetValueOrDefault();
 
-            if (featureEnabled == true)
-                Logger.Log(LogLevel.INFO, $@"Feature flag ""{featureKey}"" is enabled for user ""{userId}"".");
-            else
-                Logger.Log(LogLevel.INFO, $@"Feature flag ""{featureKey}"" is not enabled for user ""{userId}"".");
+                    // This information is only necessary for feature tests.
+                    // For rollouts experiments and variations are an implementation detail only.
+                    if (decision.Source == FeatureDecision.DECISION_SOURCE_FEATURE_TEST)
+                    {
+                        sourceInfo["experimentKey"] = decision.Experiment.Key;
+                        sourceInfo["variationKey"] = variation.Key;
+                    }
+                    else
+                    {
+                        Logger.Log(LogLevel.INFO, $@"The user ""{userId}"" is not being experimented on feature ""{featureKey}"".");
+                    }
+                }
 
-            var decisionInfo = new Dictionary<string, object>
-            {
-                { "featureKey", featureKey },
-                { "featureEnabled", featureEnabled },
-                { "source", decision.Source },
-                { "sourceInfo", sourceInfo },
-            };
+                if (featureEnabled == true)
+                    Logger.Log(LogLevel.INFO, $@"Feature flag ""{featureKey}"" is enabled for user ""{userId}"".");
+                else
+                    Logger.Log(LogLevel.INFO, $@"Feature flag ""{featureKey}"" is not enabled for user ""{userId}"".");
 
-            NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision, DecisionNotificationTypes.FEATURE, userId,
-               userAttributes ?? new UserAttributes(), decisionInfo);
+                var decisionInfo = new Dictionary<string, object>
+                {
+                    { "featureKey", featureKey },
+                    { "featureEnabled", featureEnabled },
+                    { "source", decision.Source },
+                    { "sourceInfo", sourceInfo },
+                };
+
+                NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision, DecisionNotificationTypes.FEATURE, userId,
+                   userAttributes ?? new UserAttributes(), decisionInfo);
             return featureEnabled;
         }
 
@@ -712,6 +712,139 @@ public OptimizelyUserContext CreateUserContext(string userId,
             return new OptimizelyUserContext(this, userId, userAttributes, ErrorHandler, Logger);
         }
 
+        public OptimizelyDecision Decide(OptimizelyUserContext user,
+                              string key,
+                              List<OptimizelyDecideOption> options)
+        {
+
+            var config = ProjectConfigManager?.GetConfig();
+            if (config == null)
+            {
+                return OptimizelyDecision.NewErrorDecision(key, user, DecisionMessage.SDK_NOT_READY, ErrorHandler, Logger);
+            }
+            var userId = user.UserId;
+            var inputValues = new Dictionary<string, string>
+            {
+                { USER_ID, userId },
+            };
+
+            if (!ValidateStringInputs(inputValues))
+                return null;
+
+            var flag = config.GetFeatureFlagFromKey(key);
+            if (flag == null)
+            {
+                return OptimizelyDecision.NewErrorDecision(key,
+                    user,
+                    DecisionMessage.Reason(DecisionMessage.FLAG_KEY_INVALID, key),
+                    ErrorHandler, Logger);
+            }
+
+            var userAttributes = user.UserAttributes;
+            var decisionEventDispatched = false;
+            var allOptions = GetAllOptions(options);
+            var decisionReasons = DefaultDecisionReasons.NewInstance(allOptions);
+
+            var flagDecision = DecisionService.GetVariationForFeature(
+                flag,
+                userId,
+                config,
+                userAttributes,
+                allOptions,
+                decisionReasons);
+
+            var featureEnabled = false;
+
+            var variation = flagDecision.Variation;
+
+            if (variation != null)
+            {
+                featureEnabled = variation.FeatureEnabled.GetValueOrDefault();
+            }
+            
+            if (featureEnabled)
+            {
+                Logger.Log(LogLevel.INFO, "Feature \"" + key + "\" is enabled for user \"" + userId + "\"");
+            }
+            else
+            {
+                Logger.Log(LogLevel.INFO, "Feature \"" + key + "\" is not enabled for user \"" + userId + "\"");
+            }
+            var variableMap = new Dictionary<string, object>();
+            if (flag?.Variables != null && !allOptions.Contains(OptimizelyDecideOption.EXCLUDE_VARIABLES))
+            {
+
+                foreach (var featureVariable in flag?.Variables)
+                {
+                    string variableValue = featureVariable.DefaultValue;
+                    if (featureEnabled)
+                    {
+                        var featureVariableUsageInstance = variation.GetFeatureVariableUsageFromId(featureVariable.Id);
+                        if (featureVariableUsageInstance != null)
+                        {
+                            variableValue = featureVariableUsageInstance.Value;
+                        }
+                    }
+
+                    var typeCastedValue = GetTypeCastedVariableValue(variableValue, featureVariable.Type);
+
+                    if (typeCastedValue is OptimizelyJSON)
+                        typeCastedValue = ((OptimizelyJSON)typeCastedValue).ToDictionary();
+
+                    variableMap.Add(featureVariable.Key, typeCastedValue);
+                }
+            }
+            
+            var optimizelyJSON = new OptimizelyJSON(variableMap, ErrorHandler, Logger);
+
+            var decisionSource = flagDecision?.Source ?? FeatureDecision.DECISION_SOURCE_ROLLOUT;
+            if (!allOptions.Contains(OptimizelyDecideOption.DISABLE_DECISION_EVENT))
+            {
+
+                SendImpressionEvent(flagDecision.Experiment, variation, userId, userAttributes, config, key, decisionSource);
+                decisionEventDispatched = true;
+            }
+            var reasonsToReport = decisionReasons.ToReport();
+            var variationKey = flagDecision.Variation?.Key;
+
+            // TODO: add ruleKey values when available later. use a copy of experimentKey until then.
+            var ruleKey = flagDecision.Experiment?.Key;
+
+            var decisionInfo = new Dictionary<string, object>
+            {
+                { "flagKey", key },
+                { "enabled", featureEnabled },
+                { "variables", variableMap },
+                { "variationKey", variationKey },
+                { "ruleKey", ruleKey },
+                { "reasons", decisionReasons },
+                { "decisionEventDispatched", decisionEventDispatched },
+                { "featureEnabled", featureEnabled },
+            };
+
+            NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision, DecisionNotificationTypes.FEATURE, userId,
+               userAttributes ?? new UserAttributes(), decisionInfo);
+
+            return new OptimizelyDecision(
+                variationKey,
+                featureEnabled,
+                optimizelyJSON,
+                ruleKey,
+                key,
+                user,
+                reasonsToReport);
+        }
+
+        private List<OptimizelyDecideOption> GetAllOptions(List<OptimizelyDecideOption> options)
+        {
+            var copiedOptions = new List<OptimizelyDecideOption>(DefaultDecideOptions);
+            if (options != null)
+            {
+                copiedOptions.AddRange(options);
+            }
+            return copiedOptions;
+        }
+
         /// <summary>
         /// Sends impression event.
         /// </summary>
diff --git a/OptimizelySDK/OptimizelyUserContext.cs b/OptimizelySDK/OptimizelyUserContext.cs
index e2621f20..07834546 100644
--- a/OptimizelySDK/OptimizelyUserContext.cs
+++ b/OptimizelySDK/OptimizelyUserContext.cs
@@ -46,7 +46,7 @@ public OptimizelyUserContext(Optimizely optimizely, string userId, UserAttribute
         /// <param name="value">value An attribute value</param>
         public void SetAttribute(string key, object value)
         {
-            UserAttributes.Add(key, value);
+            UserAttributes[key] = value;
         }
 
         /// <summary>
@@ -74,7 +74,7 @@ public OptimizelyDecision Decide(string key)
         public OptimizelyDecision Decide(string key,
             List<OptimizelyDecideOption> options)
         {
-            return null;
+            return Optimizely.Decide(this, key, options);
         }
  
         /// <summary>
diff --git a/OptimizelySDK/Utils/ExperimentUtils.cs b/OptimizelySDK/Utils/ExperimentUtils.cs
index 470b014c..b6023480 100644
--- a/OptimizelySDK/Utils/ExperimentUtils.cs
+++ b/OptimizelySDK/Utils/ExperimentUtils.cs
@@ -17,6 +17,7 @@
 using OptimizelySDK.AudienceConditions;
 using OptimizelySDK.Entity;
 using OptimizelySDK.Logger;
+using OptimizelySDK.OptimizelyDecisions;
 
 namespace OptimizelySDK.Utils
 {
@@ -35,6 +36,24 @@ public static bool IsExperimentActive(Experiment experiment, ILogger logger)
             return true;
         }
 
+        /// <summary>
+        /// Check if the user meets audience conditions to be in experiment or not
+        /// </summary>
+        /// <param name="config">ProjectConfig Configuration for the project</param>
+        /// <param name="experiment">Experiment Entity representing the experiment</param>
+        /// <param name="userAttributes">Attributes of the user. Defaults to empty attributes array if not provided</param>
+        /// <param name="loggingKeyType">It can be either experiment or rule.</param>
+        /// <param name="loggingKey">In case loggingKeyType is experiment it will be experiment key or else it will be rule number.</param>
+        /// <returns>true if the user meets audience conditions to be in experiment, false otherwise.</returns>
+        public static bool DoesUserMeetAudienceConditions(ProjectConfig config,
+            Experiment experiment,
+            UserAttributes userAttributes,
+            string loggingKeyType,
+            string loggingKey,
+            ILogger logger)
+        {
+            return DoesUserMeetAudienceConditions(config, experiment, userAttributes, loggingKeyType, loggingKey, DefaultDecisionReasons.NewInstance(), logger);
+        }
 
         /// <summary>
         /// Check if the user meets audience conditions to be in experiment or not
@@ -44,12 +63,14 @@ public static bool IsExperimentActive(Experiment experiment, ILogger logger)
         /// <param name="userAttributes">Attributes of the user. Defaults to empty attributes array if not provided</param>
         /// <param name="loggingKeyType">It can be either experiment or rule.</param>
         /// <param name="loggingKey">In case loggingKeyType is experiment it will be experiment key or else it will be rule number.</param>
+        /// <param name="reasons">Decision log messages.</param>
         /// <returns>true if the user meets audience conditions to be in experiment, false otherwise.</returns>
         public static bool DoesUserMeetAudienceConditions(ProjectConfig config,
             Experiment experiment,
             UserAttributes userAttributes,
             string loggingKeyType,
             string loggingKey,
+            IDecisionReasons reasons,
             ILogger logger)
         {
             if (userAttributes == null)
@@ -73,7 +94,7 @@ public static bool DoesUserMeetAudienceConditions(ProjectConfig config,
 
             var result = expConditions.Evaluate(config, userAttributes, logger).GetValueOrDefault();
             var resultText = result.ToString().ToUpper();
-            logger.Log(LogLevel.INFO, $@"Audiences for {loggingKeyType} ""{loggingKey}"" collectively evaluated to {resultText}");
+            logger.Log(LogLevel.INFO, reasons.AddInfo($@"Audiences for {loggingKeyType} ""{loggingKey}"" collectively evaluated to {resultText}"));
             return result;
         }
     }

From 591c770e14b8a336972fbce1ec276b4c938793cf Mon Sep 17 00:00:00 2001
From: muhammadnoman <muhammadnoman@folio3.com>
Date: Fri, 13 Nov 2020 20:41:31 +0500
Subject: [PATCH 10/19] Replaced list of reasons to array. Throwing not
 implemented exceptions for not implemented functions in optimizelyUserContext
 Added detailed documentation of optimizelyUserContext and Decision class

---
 .../OptimizelyDecisionTest.cs                 |   7 +-
 .../OptimizelyUserContextTest.cs              | 108 ++++++++++++++++++
 OptimizelySDK/Optimizely.cs                   |  12 +-
 .../DefaultDecisionReasons.cs                 |   1 -
 .../ErrorsDecisionReasons.cs                  |   3 +
 .../OptimizelyDecisions/OptimizelyDecision.cs |  22 +++-
 OptimizelySDK/OptimizelyUserContext.cs        |  22 +++-
 7 files changed, 157 insertions(+), 18 deletions(-)
 create mode 100644 OptimizelySDK.Tests/OptimizelyUserContextTest.cs

diff --git a/OptimizelySDK.Tests/OptimizelyDecisions/OptimizelyDecisionTest.cs b/OptimizelySDK.Tests/OptimizelyDecisions/OptimizelyDecisionTest.cs
index b430e4ea..f975e462 100644
--- a/OptimizelySDK.Tests/OptimizelyDecisions/OptimizelyDecisionTest.cs
+++ b/OptimizelySDK.Tests/OptimizelyDecisions/OptimizelyDecisionTest.cs
@@ -55,7 +55,7 @@ public void TestNewErrorDecision()
         [Test]
         public void TestNewDecision()
         {
-            var map = new Dictionary<string, object>() {
+            var variableMap = new Dictionary<string, object>() {
                 { "strField", "john doe" },
                 { "intField", 12 },
                 { "objectField", new Dictionary<string, object> () {
@@ -63,7 +63,7 @@ public void TestNewDecision()
                     }
                 }
             };
-            var optimizelyJSONUsingMap = new OptimizelyJSON(map, ErrorHandlerMock.Object, LoggerMock.Object);
+            var optimizelyJSONUsingMap = new OptimizelyJSON(variableMap, ErrorHandlerMock.Object, LoggerMock.Object);
             string expectedStringObj = "{\"strField\":\"john doe\",\"intField\":12,\"objectField\":{\"inner_field_int\":3}}";
 
             var optimizelyDecision = new OptimizelyDecision("var_key",
@@ -72,7 +72,7 @@ public void TestNewDecision()
                 "experiment",
                 "feature_key",
                 null,
-                new List<string>());
+                new string[0]);
             Assert.AreEqual(optimizelyDecision.VariationKey, "var_key");
             Assert.AreEqual(optimizelyDecision.FlagKey, "feature_key");
             Assert.AreEqual(optimizelyDecision.Variables.ToString(), expectedStringObj);
@@ -86,6 +86,7 @@ public void TestNewDecisionReasonWithDecideAllOptions()
         {
             var decisionReasons = DefaultDecisionReasons.NewInstance(new List<OptimizelyDecideOption>() { OptimizelyDecideOption.INCLUDE_REASONS });
             decisionReasons.AddError(DecisionMessage.Reason(DecisionMessage.FLAG_KEY_INVALID, "invalid_key"));
+            
             Assert.AreEqual(decisionReasons.ToReport()[0], "No flag was found for key \"invalid_key\".");
             decisionReasons.AddError(DecisionMessage.Reason(DecisionMessage.VARIABLE_VALUE_INVALID, "invalid_key"));
             Assert.AreEqual(decisionReasons.ToReport()[1], "Variable value for key \"invalid_key\" is invalid or wrong type.");
diff --git a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs
new file mode 100644
index 00000000..7a0d67bf
--- /dev/null
+++ b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs
@@ -0,0 +1,108 @@
+/**
+ *
+ *    Copyright 2020, Optimizely and contributors
+ *
+ *    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 Moq;
+using NUnit.Framework;
+using OptimizelySDK.Entity;
+using OptimizelySDK.ErrorHandler;
+using OptimizelySDK.Event.Dispatcher;
+using OptimizelySDK.Logger;
+using System;
+
+namespace OptimizelySDK.Tests
+{
+    [TestFixture]
+    public class OptimizelyUserContextTest
+    {
+        string UserID = "testUserID";
+        private Optimizely Optimizely;
+        private Mock<ILogger> LoggerMock;
+        private Mock<IErrorHandler> ErrorHandlerMock;
+        private Mock<IEventDispatcher> EventDispatcherMock;
+
+        [SetUp]
+        public void SetUp()
+        {
+            LoggerMock = new Mock<ILogger>();
+            LoggerMock.Setup(i => i.Log(It.IsAny<LogLevel>(), It.IsAny<string>()));
+
+            ErrorHandlerMock = new Mock<IErrorHandler>();
+            ErrorHandlerMock.Setup(e => e.HandleError(It.IsAny<Exception>()));
+            EventDispatcherMock = new Mock<IEventDispatcher>();
+
+            Optimizely = new Optimizely(TestData.Datafile, EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object);
+        }
+
+        [Test]
+        public void OptimizelyUserContext_withAttributes()
+        {
+            var attributes = new UserAttributes() { { "house", "GRYFFINDOR" } };
+            OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object);
+
+            Assert.AreEqual(user.Optimizely, Optimizely);
+            Assert.AreEqual(user.UserId, UserID);
+            Assert.AreEqual(user.UserAttributes, attributes);
+        }
+
+        [Test]
+        public void OptimizelyUserContext_noAttributes()
+        {
+            OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, null, ErrorHandlerMock.Object, LoggerMock.Object);
+
+            Assert.AreEqual(user.Optimizely, Optimizely);
+            Assert.AreEqual(user.UserId, UserID);
+            Assert.True(user.UserAttributes.Count == 0);
+        }
+
+        [Test]
+        public void SetAttribute()
+        {
+            var attributes = new UserAttributes() { { "house", "GRYFFINDOR" } };
+            OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object);
+
+            user.SetAttribute("k1", "v1");
+            user.SetAttribute("k2", true);
+            user.SetAttribute("k3", 100);
+            user.SetAttribute("k4", 3.5);
+
+            Assert.AreEqual(user.Optimizely, Optimizely);
+            Assert.AreEqual(user.UserId, UserID);
+            var newAttributes = user.UserAttributes;
+            Assert.AreEqual(newAttributes["house"], "GRYFFINDOR");
+            Assert.AreEqual(newAttributes["k1"], "v1");
+            Assert.AreEqual(newAttributes["k2"], true);
+            Assert.AreEqual(newAttributes["k3"], 100);
+            Assert.AreEqual(newAttributes["k4"], 3.5);
+        }
+
+        [Test]
+        public void SetAttribute_noAttribute()
+        {
+            OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, null, ErrorHandlerMock.Object, LoggerMock.Object);
+
+            user.SetAttribute("k1", "v1");
+            user.SetAttribute("k2", true);
+
+            Assert.AreEqual(user.Optimizely, Optimizely);
+            Assert.AreEqual(user.UserId, UserID);
+            var newAttributes = user.UserAttributes;
+            Assert.AreEqual(newAttributes["k1"], "v1");
+            Assert.AreEqual(newAttributes["k2"], true);
+        }
+
+    }
+}
diff --git a/OptimizelySDK/Optimizely.cs b/OptimizelySDK/Optimizely.cs
index b30d00ac..9a05ebf3 100644
--- a/OptimizelySDK/Optimizely.cs
+++ b/OptimizelySDK/Optimizely.cs
@@ -60,7 +60,7 @@ public class Optimizely : IOptimizely, IDisposable
 
         private EventProcessor EventProcessor;
 
-        private List<OptimizelyDecideOption> DefaultDecideOptions;
+        private OptimizelyDecideOption[] DefaultDecideOptions;
 
         /// <summary>
         /// It returns true if the ProjectConfig is valid otherwise false.
@@ -162,7 +162,7 @@ public Optimizely(ProjectConfigManager configManager,
                          IErrorHandler errorHandler = null,
                          UserProfileService userProfileService = null,
                          EventProcessor eventProcessor = null,
-                         List<OptimizelyDecideOption> defaultDecideOptions = null)
+                         OptimizelyDecideOption[] defaultDecideOptions = null)
         {
             ProjectConfigManager = configManager;
 
@@ -174,8 +174,8 @@ private void InitializeComponents(IEventDispatcher eventDispatcher = null,
                          IErrorHandler errorHandler = null,
                          UserProfileService userProfileService = null,
                          NotificationCenter notificationCenter = null,
-                         EventProcessor eventProcessor = null, 
-                         List<OptimizelyDecideOption> defaultDecideOptions = null)
+                         EventProcessor eventProcessor = null,
+                         OptimizelyDecideOption[] defaultDecideOptions = null)
         {
             Logger = logger ?? new NoOpLogger();
             EventDispatcher = eventDispatcher ?? new DefaultEventDispatcher(Logger);
@@ -186,7 +186,7 @@ private void InitializeComponents(IEventDispatcher eventDispatcher = null,
             NotificationCenter = notificationCenter ?? new NotificationCenter(Logger);
             DecisionService = new DecisionService(Bucketer, ErrorHandler, userProfileService, Logger);
             EventProcessor = eventProcessor ?? new ForwardingEventProcessor(EventDispatcher, NotificationCenter, Logger);
-            DefaultDecideOptions = defaultDecideOptions ?? new List<OptimizelyDecideOption>();
+            DefaultDecideOptions = defaultDecideOptions ?? new OptimizelyDecideOption[0];
         }
 
         /// <summary>
@@ -688,8 +688,6 @@ public OptimizelyJSON GetFeatureVariableJSON(string featureKey, string variableK
             return GetFeatureVariableValueForType<OptimizelyJSON>(featureKey, variableKey, userId, userAttributes, FeatureVariable.JSON_TYPE);
         }
 
-        //============ decide ============//
-
         /// <summary>
         /// Create a context of the user for which decision APIs will be called.
         /// A user context will be created successfully even when the SDK is not fully configured yet.
diff --git a/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs b/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs
index fe1a38a8..95627330 100644
--- a/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs
+++ b/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs
@@ -15,7 +15,6 @@
  */
 
 using System.Collections.Generic;
-using System.Linq;
 
 namespace OptimizelySDK.OptimizelyDecisions
 {
diff --git a/OptimizelySDK/OptimizelyDecisions/ErrorsDecisionReasons.cs b/OptimizelySDK/OptimizelyDecisions/ErrorsDecisionReasons.cs
index 890416ea..56e98d7e 100644
--- a/OptimizelySDK/OptimizelyDecisions/ErrorsDecisionReasons.cs
+++ b/OptimizelySDK/OptimizelyDecisions/ErrorsDecisionReasons.cs
@@ -18,6 +18,9 @@
 
 namespace OptimizelySDK.OptimizelyDecisions
 {
+    /// <summary>
+    /// NewErrorDecision returns a decision with errors only
+    /// </summary>
     public class ErrorsDecisionReasons : IDecisionReasons
     {
         private readonly List<string> errors = new List<string>();
diff --git a/OptimizelySDK/OptimizelyDecisions/OptimizelyDecision.cs b/OptimizelySDK/OptimizelyDecisions/OptimizelyDecision.cs
index 50334cf2..a4a934d3 100644
--- a/OptimizelySDK/OptimizelyDecisions/OptimizelyDecision.cs
+++ b/OptimizelySDK/OptimizelyDecisions/OptimizelyDecision.cs
@@ -20,15 +20,25 @@
 
 namespace OptimizelySDK.OptimizelyDecisions
 {
+    /// <summary>
+    /// OptimizelyDecision defines the decision returned by decide api.
+    /// </summary>
     public class OptimizelyDecision
     {
+        // variation key for optimizely decision.
         public string VariationKey { get; private set; }
+        // boolean value indicating if the flag is enabled or not.
         public bool Enabled { get; private set; }
+        // collection of variables associated with the decision.
         public OptimizelyJSON Variables { get; private set; }
+        // rule key of the decision.
         public string RuleKey { get; private set; }
+        // flag key for which the decision was made.
         public string FlagKey { get; private set; }
+        // user context for which the  decision was made.
         public OptimizelyUserContext UserContext { get; private set; }
-        public List<string> Reasons { get; private set; }
+        // an array of error/info/debug messages describing why the decision has been made.
+        public string[] Reasons { get; private set; }
 
         public OptimizelyDecision(string variationKey,
                               bool enabled,
@@ -36,7 +46,7 @@ public OptimizelyDecision(string variationKey,
                               string ruleKey,
                               string flagKey,
                               OptimizelyUserContext userContext,
-                              List<string> reasons)
+                              string[] reasons)
         {
             VariationKey = variationKey;
             Enabled = enabled;
@@ -47,6 +57,12 @@ public OptimizelyDecision(string variationKey,
             Reasons = reasons;
         }
 
+        /// <summary>
+        /// Static function to return OptimizelyDecision
+        /// when there are errors for example like OptimizelyConfig is not valid, etc.
+        /// OptimizelyDecision will have null variation key, false enabled, empty variables, null rule key
+        /// and error reason array
+        /// </summary>
         public static OptimizelyDecision NewErrorDecision(string key,
             OptimizelyUserContext optimizelyUserContext,
             string error,
@@ -60,7 +76,7 @@ public static OptimizelyDecision NewErrorDecision(string key,
                 null,
                 key,
                 optimizelyUserContext,
-                new List<string>() { error });
+                new string[] { error });
         }
     }
 }
diff --git a/OptimizelySDK/OptimizelyUserContext.cs b/OptimizelySDK/OptimizelyUserContext.cs
index e2621f20..c0b49df7 100644
--- a/OptimizelySDK/OptimizelyUserContext.cs
+++ b/OptimizelySDK/OptimizelyUserContext.cs
@@ -19,15 +19,22 @@
 using OptimizelySDK.ErrorHandler;
 using OptimizelySDK.Entity;
 using OptimizelySDK.OptimizelyDecisions;
+using System;
 
 namespace OptimizelySDK
 {
+    /// <summary>
+    /// OptimizelyUserContext defines user contexts that the SDK will use to make decisions for
+    /// </summary>
     public class OptimizelyUserContext
     {
         private ILogger Logger;
         private IErrorHandler ErrorHandler;
+        // userID for Optimizely user context
         public string UserId { get; }
+        // user attributes for Optimizely user context.
         public UserAttributes UserAttributes { get; }
+        // Optimizely object to be used.
         public Optimizely Optimizely { get; }
 
         public OptimizelyUserContext(Optimizely optimizely, string userId, UserAttributes userAttributes, IErrorHandler errorHandler, ILogger logger)
@@ -46,7 +53,14 @@ public OptimizelyUserContext(Optimizely optimizely, string userId, UserAttribute
         /// <param name="value">value An attribute value</param>
         public void SetAttribute(string key, object value)
         {
-            UserAttributes.Add(key, value);
+            if (key == null)
+            {
+                Logger.Log(LogLevel.WARN, "Null attribute key.");
+            }
+            else
+            { 
+                UserAttributes.Add(key, value);
+            }
         }
 
         /// <summary>
@@ -74,7 +88,7 @@ public OptimizelyDecision Decide(string key)
         public OptimizelyDecision Decide(string key,
             List<OptimizelyDecideOption> options)
         {
-            return null;
+            throw new NotImplementedException();
         }
  
         /// <summary>
@@ -84,7 +98,7 @@ public OptimizelyDecision Decide(string key,
         /// <returns>A dictionary of all decision results, mapped by flag keys.</returns>
         public Dictionary<string, OptimizelyDecision> DecideForKeys(List<string> keys)
         {
-            return null;
+            throw new NotImplementedException();
         }
 
         /// <summary>
@@ -104,7 +118,7 @@ public Dictionary<string, OptimizelyDecision> DecideAll()
         /// <returns>All decision results mapped by flag keys.</returns>
         public Dictionary<string, OptimizelyDecision> DecideAll(List<OptimizelyDecideOption> options)
         {
-            return null;
+            throw new NotImplementedException();
         }
 
         /// <summary>

From 055bb93ded6e3fff94078a272664f96513f32d8f Mon Sep 17 00:00:00 2001
From: muhammadnoman <muhammadnoman@folio3.com>
Date: Fri, 13 Nov 2020 21:47:52 +0500
Subject: [PATCH 11/19] Resolved comments of adding null check before reasons

---
 OptimizelySDK.Tests/BucketerTest.cs           |  2 +-
 .../OptimizelyUserContextTest.cs              | 29 +++++++-------
 OptimizelySDK/Bucketing/Bucketer.cs           | 10 ++---
 OptimizelySDK/Bucketing/DecisionService.cs    | 40 +++++++++----------
 OptimizelySDK/Optimizely.cs                   | 20 +++++-----
 OptimizelySDK/Utils/ExperimentUtils.cs        |  2 +-
 6 files changed, 53 insertions(+), 50 deletions(-)

diff --git a/OptimizelySDK.Tests/BucketerTest.cs b/OptimizelySDK.Tests/BucketerTest.cs
index 78b0feec..1bf4548b 100644
--- a/OptimizelySDK.Tests/BucketerTest.cs
+++ b/OptimizelySDK.Tests/BucketerTest.cs
@@ -217,4 +217,4 @@ public void TestBucketRolloutRule()
                 bucketer.Bucket(Config, rolloutRule, "testBucketingId", TestUserId, DefaultDecisionReasons.NewInstance())));
         }
     }
-}
\ No newline at end of file
+}
diff --git a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs
index 029c8499..5b1c9aa3 100644
--- a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs
+++ b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs
@@ -51,7 +51,7 @@ public void SetUp()
         }
 
         [Test]
-        public void OptimizelyUserContext_withAttributes()
+        public void OptimizelyUserContextWithAttributes()
         {
             var attributes = new UserAttributes() { { "house", "GRYFFINDOR" } };
             OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object);
@@ -62,7 +62,7 @@ public void OptimizelyUserContext_withAttributes()
         }
 
         [Test]
-        public void OptimizelyUserContext_noAttributes()
+        public void OptimizelyUserContextNoAttributes()
         {
             OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, null, ErrorHandlerMock.Object, LoggerMock.Object);
 
@@ -93,7 +93,7 @@ public void SetAttribute()
         }
 
         [Test]
-        public void SetAttribute_noAttribute()
+        public void SetAttributeNoAttribute()
         {
             OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, null, ErrorHandlerMock.Object, LoggerMock.Object);
 
@@ -108,7 +108,7 @@ public void SetAttribute_noAttribute()
         }
 
         [Test]
-        public void SetAttribute_override()
+        public void SetAttributeOverride()
         {
             var attributes = new UserAttributes() { { "house", "GRYFFINDOR" } };
             OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object);
@@ -122,7 +122,7 @@ public void SetAttribute_override()
         }
 
         [Test]
-        public void SetAttribute_nullValue()
+        public void SetAttributeNullValue()
         {
             var attributes = new UserAttributes() { { "k1", null } };
             OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object);
@@ -142,7 +142,7 @@ public void SetAttribute_nullValue()
         #region decide
 
         [Test]
-        public void Decide()
+        public void TestDecide()
         {
             var flagKey = "multi_variate_feature";
             var variablesExpected = Optimizely.GetAllFeatureVariables(flagKey, UserID);
@@ -167,19 +167,20 @@ public void DecideInvalidFlagKey()
 
             var user = Optimizely.CreateUserContext(UserID);
             user.SetAttribute("browser_type", "chrome");
+
+            var decisionExpected = OptimizelyDecision.NewErrorDecision(
+                flagKey,
+                user,
+                DecisionMessage.Reason(DecisionMessage.FLAG_KEY_INVALID, flagKey),
+                ErrorHandlerMock.Object,
+                LoggerMock.Object);
             var decision = user.Decide(flagKey);
 
-            Assert.Null(decision.VariationKey);
-            Assert.False(decision.Enabled);
-            Assert.AreEqual(decision.Variables.ToDictionary(), new Dictionary<string, object>());
-            Assert.IsNull(decision.RuleKey);
-            Assert.AreEqual(decision.FlagKey, flagKey);
-            Assert.AreEqual(decision.UserContext, user);
-            Assert.True(decision.Reasons.IsNullOrEmpty());
+            Assert.IsTrue(TestData.CompareObjects(decision, decisionExpected));
         }
 
         [Test]
-        public void DecideInvalidConfig()
+        public void DecideWhenConfigIsNull()
         {
             Optimizely optimizely = new Optimizely(TestData.UnsupportedVersionDatafile, EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object);
 
diff --git a/OptimizelySDK/Bucketing/Bucketer.cs b/OptimizelySDK/Bucketing/Bucketer.cs
index be62727a..1765453d 100644
--- a/OptimizelySDK/Bucketing/Bucketer.cs
+++ b/OptimizelySDK/Bucketing/Bucketer.cs
@@ -124,26 +124,26 @@ public virtual Variation Bucket(ProjectConfig config, Experiment experiment, str
                 if (string.IsNullOrEmpty(userExperimentId))
                 {
                     message = $"User [{userId}] is in no experiment.";
-                    Logger.Log(LogLevel.INFO, reasons.AddInfo(message));
+                    Logger.Log(LogLevel.INFO, reasons?.AddInfo(message));
                     return new Variation();
                 }
 
                 if (userExperimentId != experiment.Id)
                 {
                     message = $"User [{userId}] is not in experiment [{experiment.Key}] of group [{experiment.GroupId}].";
-                    Logger.Log(LogLevel.INFO, reasons.AddInfo(message));
+                    Logger.Log(LogLevel.INFO, reasons?.AddInfo(message));
                     return new Variation();
                 }
 
                 message = $"User [{userId}] is in experiment [{experiment.Key}] of group [{experiment.GroupId}].";
-                Logger.Log(LogLevel.INFO, reasons.AddInfo(message));
+                Logger.Log(LogLevel.INFO, reasons?.AddInfo(message));
             }
 
             // Bucket user if not in whitelist and in group (if any).
             string variationId = FindBucket(bucketingId, userId, experiment.Id, experiment.TrafficAllocation);
             if (string.IsNullOrEmpty(variationId))
             {
-                Logger.Log(LogLevel.INFO, reasons.AddInfo($"User [{userId}] is in no variation."));
+                Logger.Log(LogLevel.INFO, reasons?.AddInfo($"User [{userId}] is in no variation."));
                 return new Variation();
 
             }
@@ -151,7 +151,7 @@ public virtual Variation Bucket(ProjectConfig config, Experiment experiment, str
             // success!
             variation = config.GetVariationFromId(experiment.Key, variationId);
             message = $"User [{userId}] is in variation [{variation.Key}] of experiment [{experiment.Key}].";
-            Logger.Log(LogLevel.INFO, reasons.AddInfo(message));
+            Logger.Log(LogLevel.INFO, reasons?.AddInfo(message));
             return variation;
         }
     }
diff --git a/OptimizelySDK/Bucketing/DecisionService.cs b/OptimizelySDK/Bucketing/DecisionService.cs
index 3973b13e..542d5d22 100644
--- a/OptimizelySDK/Bucketing/DecisionService.cs
+++ b/OptimizelySDK/Bucketing/DecisionService.cs
@@ -133,16 +133,16 @@ public virtual Variation GetVariation(Experiment experiment,
                     }
                     else if (userProfileMap == null)
                     {
-                        Logger.Log(LogLevel.INFO, reasons.AddInfo("We were unable to get a user profile map from the UserProfileService."));
+                        Logger.Log(LogLevel.INFO, reasons?.AddInfo("We were unable to get a user profile map from the UserProfileService."));
                     }
                     else
                     {
-                        Logger.Log(LogLevel.ERROR, reasons.AddInfo("The UserProfileService returned an invalid map."));
+                        Logger.Log(LogLevel.ERROR, reasons?.AddInfo("The UserProfileService returned an invalid map."));
                     }
                 }
                 catch (Exception exception)
                 {
-                    Logger.Log(LogLevel.ERROR, reasons.AddInfo(exception.Message));
+                    Logger.Log(LogLevel.ERROR, reasons?.AddInfo(exception.Message));
                     ErrorHandler.HandleError(new Exceptions.OptimizelyRuntimeException(exception.Message));
                 }
             }
@@ -168,7 +168,7 @@ public virtual Variation GetVariation(Experiment experiment,
 
                 return variation;
             }
-            Logger.Log(LogLevel.INFO, reasons.AddInfo($"User \"{userId}\" does not meet conditions to be in experiment \"{experiment.Key}\"."));
+            Logger.Log(LogLevel.INFO, reasons?.AddInfo($"User \"{userId}\" does not meet conditions to be in experiment \"{experiment.Key}\"."));
 
             return null;
         }
@@ -227,7 +227,7 @@ public Variation GetForcedVariation(string experimentKey, string userId, Project
             // this case is logged in getVariationFromKey   
             if (string.IsNullOrEmpty(variationKey))
                 return null;
-            Logger.Log(LogLevel.DEBUG, reasons.AddInfo($@"Variation ""{variationKey}"" is mapped to experiment ""{experimentKey}"" and user ""{userId}"" in the forced variation map"));
+            Logger.Log(LogLevel.DEBUG, reasons?.AddInfo($@"Variation ""{variationKey}"" is mapped to experiment ""{experimentKey}"" and user ""{userId}"" in the forced variation map"));
 
             Variation variation = config.GetVariationFromKey(experimentKey, variationKey);
 
@@ -317,9 +317,9 @@ public Variation GetWhitelistedVariation(Experiment experiment, string userId, I
                 : null;
 
             if (forcedVariation != null)
-                Logger.Log(LogLevel.INFO, reasons.AddInfo($"User \"{userId}\" is forced in variation \"{forcedVariationKey}\"."));
+                Logger.Log(LogLevel.INFO, reasons?.AddInfo($"User \"{userId}\" is forced in variation \"{forcedVariationKey}\"."));
             else
-                Logger.Log(LogLevel.ERROR, reasons.AddInfo($"Variation \"{forcedVariationKey}\" is not in the datafile. Not activating user \"{userId}\"."));
+                Logger.Log(LogLevel.ERROR, reasons?.AddInfo($"Variation \"{forcedVariationKey}\" is not in the datafile. Not activating user \"{userId}\"."));
 
             return forcedVariation;
         }
@@ -342,7 +342,7 @@ public Variation GetStoredVariation(Experiment experiment, UserProfile userProfi
 
             if (decision == null)
             {
-                Logger.Log(LogLevel.INFO, reasons.AddInfo($"No previously activated variation of experiment \"{experimentKey}\" for user \"{userProfile.UserId}\" found in user profile."));
+                Logger.Log(LogLevel.INFO, reasons?.AddInfo($"No previously activated variation of experiment \"{experimentKey}\" for user \"{userProfile.UserId}\" found in user profile."));
                 return null;
             }
 
@@ -356,11 +356,11 @@ public Variation GetStoredVariation(Experiment experiment, UserProfile userProfi
 
                 if (savedVariation == null)
                 {
-                    Logger.Log(LogLevel.INFO, reasons.AddInfo($"User \"{userProfile.UserId}\" was previously bucketed into variation with ID \"{variationId}\" for experiment \"{experimentId}\", but no matching variation was found for that user. We will re-bucket the user."));
+                    Logger.Log(LogLevel.INFO, reasons?.AddInfo($"User \"{userProfile.UserId}\" was previously bucketed into variation with ID \"{variationId}\" for experiment \"{experimentId}\", but no matching variation was found for that user. We will re-bucket the user."));
                     return null;
                 }
 
-                Logger.Log(LogLevel.INFO, reasons.AddInfo($"Returning previously activated variation \"{savedVariation.Key}\" of experiment \"{experimentKey}\" for user \"{userProfile.UserId}\" from user profile."));
+                Logger.Log(LogLevel.INFO, reasons?.AddInfo($"Returning previously activated variation \"{savedVariation.Key}\" of experiment \"{experimentKey}\" for user \"{userProfile.UserId}\" from user profile."));
                 return savedVariation;
             }
             catch (Exception)
@@ -407,11 +407,11 @@ public void SaveVariation(Experiment experiment, Variation variation, UserProfil
             try
             {
                 UserProfileService.Save(userProfile.ToMap());
-                Logger.Log(LogLevel.INFO, reasons.AddInfo($"Saved variation \"{variation.Id}\" of experiment \"{experiment.Id}\" for user \"{userProfile.UserId}\"."));
+                Logger.Log(LogLevel.INFO, reasons?.AddInfo($"Saved variation \"{variation.Id}\" of experiment \"{experiment.Id}\" for user \"{userProfile.UserId}\"."));
             }
             catch (Exception exception)
             {
-                Logger.Log(LogLevel.ERROR, reasons.AddInfo($"Failed to save variation \"{variation.Id}\" of experiment \"{experiment.Id}\" for user \"{userProfile.UserId}\"."));
+                Logger.Log(LogLevel.ERROR, reasons?.AddInfo($"Failed to save variation \"{variation.Id}\" of experiment \"{experiment.Id}\" for user \"{userProfile.UserId}\"."));
                 ErrorHandler.HandleError(new Exceptions.OptimizelyRuntimeException(exception.Message));
             }
         }
@@ -441,7 +441,7 @@ public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag feature
 
             if (string.IsNullOrEmpty(featureFlag.RolloutId))
             {
-                Logger.Log(LogLevel.INFO, reasons.AddInfo($"The feature flag \"{featureFlag.Key}\" is not used in a rollout."));
+                Logger.Log(LogLevel.INFO, reasons?.AddInfo($"The feature flag \"{featureFlag.Key}\" is not used in a rollout."));
                 return null;
             }
 
@@ -449,7 +449,7 @@ public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag feature
 
             if (string.IsNullOrEmpty(rollout.Id))
             {
-                Logger.Log(LogLevel.ERROR, reasons.AddInfo($"The rollout with id \"{featureFlag.RolloutId}\" is not found in the datafile for feature flag \"{featureFlag.Key}\""));
+                Logger.Log(LogLevel.ERROR, reasons?.AddInfo($"The rollout with id \"{featureFlag.RolloutId}\" is not found in the datafile for feature flag \"{featureFlag.Key}\""));
                 return null;
             }
 
@@ -526,7 +526,7 @@ public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag feat
 
             if (featureFlag.ExperimentIds == null || featureFlag.ExperimentIds.Count == 0)
             {
-                Logger.Log(LogLevel.INFO, reasons.AddInfo($"The feature flag \"{featureFlag.Key}\" is not used in any experiments."));
+                Logger.Log(LogLevel.INFO, reasons?.AddInfo($"The feature flag \"{featureFlag.Key}\" is not used in any experiments."));
                 return null;
             }
 
@@ -541,12 +541,12 @@ public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag feat
 
                 if (variation != null && !string.IsNullOrEmpty(variation.Id))
                 {
-                    Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is bucketed into experiment \"{experiment.Key}\" of feature \"{featureFlag.Key}\"."));
+                    Logger.Log(LogLevel.INFO, reasons?.AddInfo($"The user \"{userId}\" is bucketed into experiment \"{experiment.Key}\" of feature \"{featureFlag.Key}\"."));
                     return new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST);
                 }
             }
 
-            Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is not bucketed into any of the experiments on the feature \"{featureFlag.Key}\"."));
+            Logger.Log(LogLevel.INFO, reasons?.AddInfo($"The user \"{userId}\" is not bucketed into any of the experiments on the feature \"{featureFlag.Key}\"."));
             return null;
         }
 
@@ -592,11 +592,11 @@ public virtual FeatureDecision GetVariationForFeature(FeatureFlag featureFlag,
 
             if (decision != null)
             {
-                Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is bucketed into a rollout for feature flag \"{featureFlag.Key}\"."));
+                Logger.Log(LogLevel.INFO, reasons?.AddInfo($"The user \"{userId}\" is bucketed into a rollout for feature flag \"{featureFlag.Key}\"."));
                 return decision;
             }
 
-            Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is not bucketed into a rollout for feature flag \"{featureFlag.Key}\"."));
+            Logger.Log(LogLevel.INFO, reasons?.AddInfo($"The user \"{userId}\" is not bucketed into a rollout for feature flag \"{featureFlag.Key}\"."));
             return new FeatureDecision(null, null, FeatureDecision.DECISION_SOURCE_ROLLOUT);
         }
 
@@ -620,7 +620,7 @@ private string GetBucketingId(string userId, UserAttributes filteredAttributes,
                 }
                 else
                 {
-                    Logger.Log(LogLevel.WARN, reasons.AddInfo("BucketingID attribute is not a string. Defaulted to userId"));
+                    Logger.Log(LogLevel.WARN, reasons?.AddInfo("BucketingID attribute is not a string. Defaulted to userId"));
                 }
             }
 
diff --git a/OptimizelySDK/Optimizely.cs b/OptimizelySDK/Optimizely.cs
index 0e4f156c..df386559 100644
--- a/OptimizelySDK/Optimizely.cs
+++ b/OptimizelySDK/Optimizely.cs
@@ -712,6 +712,15 @@ public OptimizelyUserContext CreateUserContext(string userId,
             return new OptimizelyUserContext(this, userId, userAttributes, ErrorHandler, Logger);
         }
 
+        /// <summary>
+        /// Returns a decision result ({@link OptimizelyDecision}) for a given flag key and a user context, which contains all data required to deliver the flag.
+        /// <ul>
+        /// <li>If the SDK finds an error, it’ll return a decision with <b>null</b> for <b>variationKey</b>. The decision will include an error message in <b>reasons</b>.
+        /// </ul>
+        /// </summary>
+        /// <param name="key">A flag key for which a decision will be made.</param>
+        /// <param name="options">A list of options for decision-making.</param>
+        /// <returns>A decision result.</returns>
         public OptimizelyDecision Decide(OptimizelyUserContext user,
                               string key,
                               List<OptimizelyDecideOption> options)
@@ -723,16 +732,9 @@ public OptimizelyDecision Decide(OptimizelyUserContext user,
                 return OptimizelyDecision.NewErrorDecision(key, user, DecisionMessage.SDK_NOT_READY, ErrorHandler, Logger);
             }
             var userId = user.UserId;
-            var inputValues = new Dictionary<string, string>
-            {
-                { USER_ID, userId },
-            };
-
-            if (!ValidateStringInputs(inputValues))
-                return null;
-
+            
             var flag = config.GetFeatureFlagFromKey(key);
-            if (flag == null)
+            if (flag.Key == null)
             {
                 return OptimizelyDecision.NewErrorDecision(key,
                     user,
diff --git a/OptimizelySDK/Utils/ExperimentUtils.cs b/OptimizelySDK/Utils/ExperimentUtils.cs
index b6023480..593718b1 100644
--- a/OptimizelySDK/Utils/ExperimentUtils.cs
+++ b/OptimizelySDK/Utils/ExperimentUtils.cs
@@ -94,7 +94,7 @@ public static bool DoesUserMeetAudienceConditions(ProjectConfig config,
 
             var result = expConditions.Evaluate(config, userAttributes, logger).GetValueOrDefault();
             var resultText = result.ToString().ToUpper();
-            logger.Log(LogLevel.INFO, reasons.AddInfo($@"Audiences for {loggingKeyType} ""{loggingKey}"" collectively evaluated to {resultText}"));
+            logger.Log(LogLevel.INFO, reasons?.AddInfo($@"Audiences for {loggingKeyType} ""{loggingKey}"" collectively evaluated to {resultText}"));
             return result;
         }
     }

From 5ce14194f015d06b5fd8e99cbcdac384fed4f5fb Mon Sep 17 00:00:00 2001
From: muhammadnoman <muhammadnoman@folio3.com>
Date: Fri, 13 Nov 2020 21:54:30 +0500
Subject: [PATCH 12/19] removed underscores

---
 OptimizelySDK.Tests/OptimizelyUserContextTest.cs | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs
index 7a0d67bf..27e0f60a 100644
--- a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs
+++ b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs
@@ -48,7 +48,7 @@ public void SetUp()
         }
 
         [Test]
-        public void OptimizelyUserContext_withAttributes()
+        public void OptimizelyUserContextWithAttributes()
         {
             var attributes = new UserAttributes() { { "house", "GRYFFINDOR" } };
             OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object);
@@ -59,7 +59,7 @@ public void OptimizelyUserContext_withAttributes()
         }
 
         [Test]
-        public void OptimizelyUserContext_noAttributes()
+        public void OptimizelyUserContextNoAttributes()
         {
             OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, null, ErrorHandlerMock.Object, LoggerMock.Object);
 
@@ -90,7 +90,7 @@ public void SetAttribute()
         }
 
         [Test]
-        public void SetAttribute_noAttribute()
+        public void SetAttributeNoAttribute()
         {
             OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, null, ErrorHandlerMock.Object, LoggerMock.Object);
 

From fafb14ce73fbbfcda44c2d0e889bb029e457cf2a Mon Sep 17 00:00:00 2001
From: muhammadnoman <muhammadnoman@folio3.com>
Date: Tue, 24 Nov 2020 19:28:52 +0500
Subject: [PATCH 13/19] Added Additional unit tests of optimizelyUserContext

---
 .../OptimizelyDecisionTest.cs                    | 14 +++++++++++++-
 OptimizelySDK.Tests/OptimizelySDK.Tests.csproj   |  1 +
 OptimizelySDK.Tests/OptimizelyUserContextTest.cs | 16 ++++++++++++++++
 .../DefaultDecisionReasons.cs                    |  6 +++---
 OptimizelySDK/OptimizelyUserContext.cs           |  4 ++--
 5 files changed, 35 insertions(+), 6 deletions(-)

diff --git a/OptimizelySDK.Tests/OptimizelyDecisions/OptimizelyDecisionTest.cs b/OptimizelySDK.Tests/OptimizelyDecisions/OptimizelyDecisionTest.cs
index f975e462..bf8f5f17 100644
--- a/OptimizelySDK.Tests/OptimizelyDecisions/OptimizelyDecisionTest.cs
+++ b/OptimizelySDK.Tests/OptimizelyDecisions/OptimizelyDecisionTest.cs
@@ -82,7 +82,7 @@ public void TestNewDecision()
         }
 
         [Test]
-        public void TestNewDecisionReasonWithDecideAllOptions()
+        public void TestNewDecisionReasonWithIncludeReasons()
         {
             var decisionReasons = DefaultDecisionReasons.NewInstance(new List<OptimizelyDecideOption>() { OptimizelyDecideOption.INCLUDE_REASONS });
             decisionReasons.AddError(DecisionMessage.Reason(DecisionMessage.FLAG_KEY_INVALID, "invalid_key"));
@@ -94,5 +94,17 @@ public void TestNewDecisionReasonWithDecideAllOptions()
             Assert.AreEqual(decisionReasons.ToReport()[2], "Some info message.");
         }
 
+        [Test]
+        public void TestNewDecisionReasonWithoutIncludeReasons()
+        {
+            var decisionReasons = DefaultDecisionReasons.NewInstance();
+            decisionReasons.AddError(DecisionMessage.Reason(DecisionMessage.FLAG_KEY_INVALID, "invalid_key"));
+
+            Assert.AreEqual(decisionReasons.ToReport()[0], "No flag was found for key \"invalid_key\".");
+            decisionReasons.AddError(DecisionMessage.Reason(DecisionMessage.VARIABLE_VALUE_INVALID, "invalid_key"));
+            Assert.AreEqual(decisionReasons.ToReport()[1], "Variable value for key \"invalid_key\" is invalid or wrong type.");
+            decisionReasons.AddInfo("Some info message.");
+            Assert.AreEqual(decisionReasons.ToReport().Count, 2);
+        }
     }
 }
diff --git a/OptimizelySDK.Tests/OptimizelySDK.Tests.csproj b/OptimizelySDK.Tests/OptimizelySDK.Tests.csproj
index 0f44902c..99a0995c 100644
--- a/OptimizelySDK.Tests/OptimizelySDK.Tests.csproj
+++ b/OptimizelySDK.Tests/OptimizelySDK.Tests.csproj
@@ -91,6 +91,7 @@
     <Compile Include="NotificationTests\NotificationCenterTests.cs" />
     <Compile Include="ClientConfigHandlerTest.cs" />
     <Compile Include="OptimizelyTest.cs" />
+    <Compile Include="OptimizelyUserContextTest.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="TestBucketer.cs" />
     <Compile Include="BucketerTest.cs" />
diff --git a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs
index 27e0f60a..1c18ff46 100644
--- a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs
+++ b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs
@@ -104,5 +104,21 @@ public void SetAttributeNoAttribute()
             Assert.AreEqual(newAttributes["k2"], true);
         }
 
+        [Test]
+        public void SetAttributeToOverrideAttribute()
+        {
+            OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, null, ErrorHandlerMock.Object, LoggerMock.Object);
+
+
+            Assert.AreEqual(user.Optimizely, Optimizely);
+            Assert.AreEqual(user.UserId, UserID);
+
+            user.SetAttribute("k1", "v1");
+            Assert.AreEqual(user.UserAttributes["k1"], "v1");
+
+            user.SetAttribute("k1", true);
+            Assert.AreEqual(user.UserAttributes["k1"], true);
+        }
+
     }
 }
diff --git a/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs b/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs
index 95627330..0ded3470 100644
--- a/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs
+++ b/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs
@@ -21,7 +21,7 @@ namespace OptimizelySDK.OptimizelyDecisions
     public class DefaultDecisionReasons : IDecisionReasons
     {
         private List<string> Errors = new List<string>();
-        private List<string> Logs = new List<string>();
+        private List<string> Infos = new List<string>();
 
         public static IDecisionReasons NewInstance(List<OptimizelyDecideOption> options)
         {
@@ -49,14 +49,14 @@ public void AddError(string format, params object[] args)
         public string AddInfo(string format, params object[] args)
         {
             string message = string.Format(format, args);
-            Logs.Add(message);
+            Infos.Add(message);
             return message;
         }
 
         public List<string> ToReport()
         {
             List<string> reasons = new List<string>(Errors);
-            reasons.AddRange(Logs);
+            reasons.AddRange(Infos);
             return reasons;
         }
     }
diff --git a/OptimizelySDK/OptimizelyUserContext.cs b/OptimizelySDK/OptimizelyUserContext.cs
index c0b49df7..869571e8 100644
--- a/OptimizelySDK/OptimizelyUserContext.cs
+++ b/OptimizelySDK/OptimizelyUserContext.cs
@@ -58,8 +58,8 @@ public void SetAttribute(string key, object value)
                 Logger.Log(LogLevel.WARN, "Null attribute key.");
             }
             else
-            { 
-                UserAttributes.Add(key, value);
+            {
+                UserAttributes[key] = value;
             }
         }
 

From 671aa294a2f9773015e3a436eb5a95e37536fc6d Mon Sep 17 00:00:00 2001
From: muhammadnoman <muhammadnoman@folio3.com>
Date: Tue, 24 Nov 2020 22:07:50 +0500
Subject: [PATCH 14/19] added notification type flag removed nullable from
 reasons and made sure its not null

---
 .../OptimizelyUserContextTest.cs              |  2 +-
 OptimizelySDK/Bucketing/Bucketer.cs           | 10 ++---
 OptimizelySDK/Bucketing/DecisionService.cs    | 42 +++++++++----------
 OptimizelySDK/Optimizely.cs                   |  5 +--
 OptimizelySDK/Utils/DecisionInfoTypes.cs      |  1 +
 OptimizelySDK/Utils/ExperimentUtils.cs        |  2 +-
 6 files changed, 31 insertions(+), 31 deletions(-)

diff --git a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs
index 28334888..1238e73e 100644
--- a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs
+++ b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs
@@ -171,7 +171,7 @@ public void TestDecide()
             Assert.AreEqual(decision.RuleKey, "test_experiment_multivariate");
             Assert.AreEqual(decision.FlagKey, flagKey);
             Assert.AreEqual(decision.UserContext, user);
-            Assert.True(decision.Reasons.IsNullOrEmpty());
+            Assert.IsNotNull(decision.Reasons);
         }
 
         [Test]
diff --git a/OptimizelySDK/Bucketing/Bucketer.cs b/OptimizelySDK/Bucketing/Bucketer.cs
index 1765453d..be62727a 100644
--- a/OptimizelySDK/Bucketing/Bucketer.cs
+++ b/OptimizelySDK/Bucketing/Bucketer.cs
@@ -124,26 +124,26 @@ public virtual Variation Bucket(ProjectConfig config, Experiment experiment, str
                 if (string.IsNullOrEmpty(userExperimentId))
                 {
                     message = $"User [{userId}] is in no experiment.";
-                    Logger.Log(LogLevel.INFO, reasons?.AddInfo(message));
+                    Logger.Log(LogLevel.INFO, reasons.AddInfo(message));
                     return new Variation();
                 }
 
                 if (userExperimentId != experiment.Id)
                 {
                     message = $"User [{userId}] is not in experiment [{experiment.Key}] of group [{experiment.GroupId}].";
-                    Logger.Log(LogLevel.INFO, reasons?.AddInfo(message));
+                    Logger.Log(LogLevel.INFO, reasons.AddInfo(message));
                     return new Variation();
                 }
 
                 message = $"User [{userId}] is in experiment [{experiment.Key}] of group [{experiment.GroupId}].";
-                Logger.Log(LogLevel.INFO, reasons?.AddInfo(message));
+                Logger.Log(LogLevel.INFO, reasons.AddInfo(message));
             }
 
             // Bucket user if not in whitelist and in group (if any).
             string variationId = FindBucket(bucketingId, userId, experiment.Id, experiment.TrafficAllocation);
             if (string.IsNullOrEmpty(variationId))
             {
-                Logger.Log(LogLevel.INFO, reasons?.AddInfo($"User [{userId}] is in no variation."));
+                Logger.Log(LogLevel.INFO, reasons.AddInfo($"User [{userId}] is in no variation."));
                 return new Variation();
 
             }
@@ -151,7 +151,7 @@ public virtual Variation Bucket(ProjectConfig config, Experiment experiment, str
             // success!
             variation = config.GetVariationFromId(experiment.Key, variationId);
             message = $"User [{userId}] is in variation [{variation.Key}] of experiment [{experiment.Key}].";
-            Logger.Log(LogLevel.INFO, reasons?.AddInfo(message));
+            Logger.Log(LogLevel.INFO, reasons.AddInfo(message));
             return variation;
         }
     }
diff --git a/OptimizelySDK/Bucketing/DecisionService.cs b/OptimizelySDK/Bucketing/DecisionService.cs
index 542d5d22..c0ffd46c 100644
--- a/OptimizelySDK/Bucketing/DecisionService.cs
+++ b/OptimizelySDK/Bucketing/DecisionService.cs
@@ -133,16 +133,16 @@ public virtual Variation GetVariation(Experiment experiment,
                     }
                     else if (userProfileMap == null)
                     {
-                        Logger.Log(LogLevel.INFO, reasons?.AddInfo("We were unable to get a user profile map from the UserProfileService."));
+                        Logger.Log(LogLevel.INFO, reasons.AddInfo("We were unable to get a user profile map from the UserProfileService."));
                     }
                     else
                     {
-                        Logger.Log(LogLevel.ERROR, reasons?.AddInfo("The UserProfileService returned an invalid map."));
+                        Logger.Log(LogLevel.ERROR, reasons.AddInfo("The UserProfileService returned an invalid map."));
                     }
                 }
                 catch (Exception exception)
                 {
-                    Logger.Log(LogLevel.ERROR, reasons?.AddInfo(exception.Message));
+                    Logger.Log(LogLevel.ERROR, reasons.AddInfo(exception.Message));
                     ErrorHandler.HandleError(new Exceptions.OptimizelyRuntimeException(exception.Message));
                 }
             }
@@ -156,7 +156,7 @@ public virtual Variation GetVariation(Experiment experiment,
 
                 if (variation != null && variation.Key != null)
                 {
-                    if (UserProfileService != null)
+                    if (UserProfileService != null && !ignoreUPS)
                     {
                         var bucketerUserProfile = userProfile ?? new UserProfile(userId, new Dictionary<string, Decision>());
                         SaveVariation(experiment, variation, bucketerUserProfile, reasons);
@@ -168,7 +168,7 @@ public virtual Variation GetVariation(Experiment experiment,
 
                 return variation;
             }
-            Logger.Log(LogLevel.INFO, reasons?.AddInfo($"User \"{userId}\" does not meet conditions to be in experiment \"{experiment.Key}\"."));
+            Logger.Log(LogLevel.INFO, reasons.AddInfo($"User \"{userId}\" does not meet conditions to be in experiment \"{experiment.Key}\"."));
 
             return null;
         }
@@ -227,7 +227,7 @@ public Variation GetForcedVariation(string experimentKey, string userId, Project
             // this case is logged in getVariationFromKey   
             if (string.IsNullOrEmpty(variationKey))
                 return null;
-            Logger.Log(LogLevel.DEBUG, reasons?.AddInfo($@"Variation ""{variationKey}"" is mapped to experiment ""{experimentKey}"" and user ""{userId}"" in the forced variation map"));
+            Logger.Log(LogLevel.DEBUG, reasons.AddInfo($@"Variation ""{variationKey}"" is mapped to experiment ""{experimentKey}"" and user ""{userId}"" in the forced variation map"));
 
             Variation variation = config.GetVariationFromKey(experimentKey, variationKey);
 
@@ -317,9 +317,9 @@ public Variation GetWhitelistedVariation(Experiment experiment, string userId, I
                 : null;
 
             if (forcedVariation != null)
-                Logger.Log(LogLevel.INFO, reasons?.AddInfo($"User \"{userId}\" is forced in variation \"{forcedVariationKey}\"."));
+                Logger.Log(LogLevel.INFO, reasons.AddInfo($"User \"{userId}\" is forced in variation \"{forcedVariationKey}\"."));
             else
-                Logger.Log(LogLevel.ERROR, reasons?.AddInfo($"Variation \"{forcedVariationKey}\" is not in the datafile. Not activating user \"{userId}\"."));
+                Logger.Log(LogLevel.ERROR, reasons.AddInfo($"Variation \"{forcedVariationKey}\" is not in the datafile. Not activating user \"{userId}\"."));
 
             return forcedVariation;
         }
@@ -342,7 +342,7 @@ public Variation GetStoredVariation(Experiment experiment, UserProfile userProfi
 
             if (decision == null)
             {
-                Logger.Log(LogLevel.INFO, reasons?.AddInfo($"No previously activated variation of experiment \"{experimentKey}\" for user \"{userProfile.UserId}\" found in user profile."));
+                Logger.Log(LogLevel.INFO, reasons.AddInfo($"No previously activated variation of experiment \"{experimentKey}\" for user \"{userProfile.UserId}\" found in user profile."));
                 return null;
             }
 
@@ -356,11 +356,11 @@ public Variation GetStoredVariation(Experiment experiment, UserProfile userProfi
 
                 if (savedVariation == null)
                 {
-                    Logger.Log(LogLevel.INFO, reasons?.AddInfo($"User \"{userProfile.UserId}\" was previously bucketed into variation with ID \"{variationId}\" for experiment \"{experimentId}\", but no matching variation was found for that user. We will re-bucket the user."));
+                    Logger.Log(LogLevel.INFO, reasons.AddInfo($"User \"{userProfile.UserId}\" was previously bucketed into variation with ID \"{variationId}\" for experiment \"{experimentId}\", but no matching variation was found for that user. We will re-bucket the user."));
                     return null;
                 }
 
-                Logger.Log(LogLevel.INFO, reasons?.AddInfo($"Returning previously activated variation \"{savedVariation.Key}\" of experiment \"{experimentKey}\" for user \"{userProfile.UserId}\" from user profile."));
+                Logger.Log(LogLevel.INFO, reasons.AddInfo($"Returning previously activated variation \"{savedVariation.Key}\" of experiment \"{experimentKey}\" for user \"{userProfile.UserId}\" from user profile."));
                 return savedVariation;
             }
             catch (Exception)
@@ -407,11 +407,11 @@ public void SaveVariation(Experiment experiment, Variation variation, UserProfil
             try
             {
                 UserProfileService.Save(userProfile.ToMap());
-                Logger.Log(LogLevel.INFO, reasons?.AddInfo($"Saved variation \"{variation.Id}\" of experiment \"{experiment.Id}\" for user \"{userProfile.UserId}\"."));
+                Logger.Log(LogLevel.INFO, $"Saved variation \"{variation.Id}\" of experiment \"{experiment.Id}\" for user \"{userProfile.UserId}\".");
             }
             catch (Exception exception)
             {
-                Logger.Log(LogLevel.ERROR, reasons?.AddInfo($"Failed to save variation \"{variation.Id}\" of experiment \"{experiment.Id}\" for user \"{userProfile.UserId}\"."));
+                Logger.Log(LogLevel.ERROR, $"Failed to save variation \"{variation.Id}\" of experiment \"{experiment.Id}\" for user \"{userProfile.UserId}\".");
                 ErrorHandler.HandleError(new Exceptions.OptimizelyRuntimeException(exception.Message));
             }
         }
@@ -441,7 +441,7 @@ public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag feature
 
             if (string.IsNullOrEmpty(featureFlag.RolloutId))
             {
-                Logger.Log(LogLevel.INFO, reasons?.AddInfo($"The feature flag \"{featureFlag.Key}\" is not used in a rollout."));
+                Logger.Log(LogLevel.INFO, reasons.AddInfo($"The feature flag \"{featureFlag.Key}\" is not used in a rollout."));
                 return null;
             }
 
@@ -449,7 +449,7 @@ public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag feature
 
             if (string.IsNullOrEmpty(rollout.Id))
             {
-                Logger.Log(LogLevel.ERROR, reasons?.AddInfo($"The rollout with id \"{featureFlag.RolloutId}\" is not found in the datafile for feature flag \"{featureFlag.Key}\""));
+                Logger.Log(LogLevel.ERROR, reasons.AddInfo($"The rollout with id \"{featureFlag.RolloutId}\" is not found in the datafile for feature flag \"{featureFlag.Key}\""));
                 return null;
             }
 
@@ -526,7 +526,7 @@ public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag feat
 
             if (featureFlag.ExperimentIds == null || featureFlag.ExperimentIds.Count == 0)
             {
-                Logger.Log(LogLevel.INFO, reasons?.AddInfo($"The feature flag \"{featureFlag.Key}\" is not used in any experiments."));
+                Logger.Log(LogLevel.INFO, reasons.AddInfo($"The feature flag \"{featureFlag.Key}\" is not used in any experiments."));
                 return null;
             }
 
@@ -541,12 +541,12 @@ public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag feat
 
                 if (variation != null && !string.IsNullOrEmpty(variation.Id))
                 {
-                    Logger.Log(LogLevel.INFO, reasons?.AddInfo($"The user \"{userId}\" is bucketed into experiment \"{experiment.Key}\" of feature \"{featureFlag.Key}\"."));
+                    Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is bucketed into experiment \"{experiment.Key}\" of feature \"{featureFlag.Key}\"."));
                     return new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST);
                 }
             }
 
-            Logger.Log(LogLevel.INFO, reasons?.AddInfo($"The user \"{userId}\" is not bucketed into any of the experiments on the feature \"{featureFlag.Key}\"."));
+            Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is not bucketed into any of the experiments on the feature \"{featureFlag.Key}\"."));
             return null;
         }
 
@@ -592,11 +592,11 @@ public virtual FeatureDecision GetVariationForFeature(FeatureFlag featureFlag,
 
             if (decision != null)
             {
-                Logger.Log(LogLevel.INFO, reasons?.AddInfo($"The user \"{userId}\" is bucketed into a rollout for feature flag \"{featureFlag.Key}\"."));
+                Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is bucketed into a rollout for feature flag \"{featureFlag.Key}\"."));
                 return decision;
             }
 
-            Logger.Log(LogLevel.INFO, reasons?.AddInfo($"The user \"{userId}\" is not bucketed into a rollout for feature flag \"{featureFlag.Key}\"."));
+            Logger.Log(LogLevel.INFO, reasons.AddInfo($"The user \"{userId}\" is not bucketed into a rollout for feature flag \"{featureFlag.Key}\"."));
             return new FeatureDecision(null, null, FeatureDecision.DECISION_SOURCE_ROLLOUT);
         }
 
@@ -620,7 +620,7 @@ private string GetBucketingId(string userId, UserAttributes filteredAttributes,
                 }
                 else
                 {
-                    Logger.Log(LogLevel.WARN, reasons?.AddInfo("BucketingID attribute is not a string. Defaulted to userId"));
+                    Logger.Log(LogLevel.WARN, reasons.AddInfo("BucketingID attribute is not a string. Defaulted to userId"));
                 }
             }
 
diff --git a/OptimizelySDK/Optimizely.cs b/OptimizelySDK/Optimizely.cs
index df386559..0b7209ed 100644
--- a/OptimizelySDK/Optimizely.cs
+++ b/OptimizelySDK/Optimizely.cs
@@ -819,11 +819,10 @@ public OptimizelyDecision Decide(OptimizelyUserContext user,
                 { "variationKey", variationKey },
                 { "ruleKey", ruleKey },
                 { "reasons", decisionReasons },
-                { "decisionEventDispatched", decisionEventDispatched },
-                { "featureEnabled", featureEnabled },
+                { "decisionEventDispatched", decisionEventDispatched }
             };
 
-            NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision, DecisionNotificationTypes.FEATURE, userId,
+            NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision, DecisionNotificationTypes.FLAG, userId,
                userAttributes ?? new UserAttributes(), decisionInfo);
 
             return new OptimizelyDecision(
diff --git a/OptimizelySDK/Utils/DecisionInfoTypes.cs b/OptimizelySDK/Utils/DecisionInfoTypes.cs
index 188522e8..10cac1f7 100644
--- a/OptimizelySDK/Utils/DecisionInfoTypes.cs
+++ b/OptimizelySDK/Utils/DecisionInfoTypes.cs
@@ -20,6 +20,7 @@ public static class DecisionNotificationTypes
     {
         public const string AB_TEST = "ab-test";
         public const string FEATURE = "feature";
+        public const string FLAG = "flag";
         public const string FEATURE_TEST = "feature-test";
         public const string FEATURE_VARIABLE = "feature-variable";
         public const string ALL_FEATURE_VARIABLE = "all-feature-variables";
diff --git a/OptimizelySDK/Utils/ExperimentUtils.cs b/OptimizelySDK/Utils/ExperimentUtils.cs
index 593718b1..b6023480 100644
--- a/OptimizelySDK/Utils/ExperimentUtils.cs
+++ b/OptimizelySDK/Utils/ExperimentUtils.cs
@@ -94,7 +94,7 @@ public static bool DoesUserMeetAudienceConditions(ProjectConfig config,
 
             var result = expConditions.Evaluate(config, userAttributes, logger).GetValueOrDefault();
             var resultText = result.ToString().ToUpper();
-            logger.Log(LogLevel.INFO, reasons?.AddInfo($@"Audiences for {loggingKeyType} ""{loggingKey}"" collectively evaluated to {resultText}"));
+            logger.Log(LogLevel.INFO, reasons.AddInfo($@"Audiences for {loggingKeyType} ""{loggingKey}"" collectively evaluated to {resultText}"));
             return result;
         }
     }

From a0753a6bb1f3d8f48d6f00bad9bad7b903f52064 Mon Sep 17 00:00:00 2001
From: muhammadnoman <muhammadnoman@folio3.com>
Date: Wed, 25 Nov 2020 19:42:14 +0500
Subject: [PATCH 15/19] Added mutex lock in setting attributes value

---
 OptimizelySDK/OptimizelyUserContext.cs | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/OptimizelySDK/OptimizelyUserContext.cs b/OptimizelySDK/OptimizelyUserContext.cs
index 869571e8..e273c7c7 100644
--- a/OptimizelySDK/OptimizelyUserContext.cs
+++ b/OptimizelySDK/OptimizelyUserContext.cs
@@ -30,6 +30,7 @@ public class OptimizelyUserContext
     {
         private ILogger Logger;
         private IErrorHandler ErrorHandler;
+        private static object mutex = new object();
         // userID for Optimizely user context
         public string UserId { get; }
         // user attributes for Optimizely user context.
@@ -59,7 +60,10 @@ public void SetAttribute(string key, object value)
             }
             else
             {
-                UserAttributes[key] = value;
+                lock(mutex) 
+                { 
+                    UserAttributes[key] = value;
+                }
             }
         }
 

From 0e9faa18afea6818d6933a037075aaf17b6f4734 Mon Sep 17 00:00:00 2001
From: muhammadnoman <muhammadnoman@folio3.com>
Date: Thu, 26 Nov 2020 13:21:23 +0500
Subject: [PATCH 16/19] replaced list from array of decideOptions

---
 OptimizelySDK.Tests/DecisionServiceTest.cs    | 28 +++++++++----------
 .../OptimizelyDecisionTest.cs                 |  2 +-
 OptimizelySDK/Bucketing/DecisionService.cs    | 12 ++++----
 OptimizelySDK/Optimizely.cs                   | 12 ++++----
 .../DefaultDecisionReasons.cs                 |  5 ++--
 OptimizelySDK/OptimizelyUserContext.cs        |  8 +++---
 6 files changed, 35 insertions(+), 32 deletions(-)

diff --git a/OptimizelySDK.Tests/DecisionServiceTest.cs b/OptimizelySDK.Tests/DecisionServiceTest.cs
index 3c83b3db..6513e6e5 100644
--- a/OptimizelySDK.Tests/DecisionServiceTest.cs
+++ b/OptimizelySDK.Tests/DecisionServiceTest.cs
@@ -469,7 +469,7 @@ public void TestGetVariationWithBucketingId()
         public void TestGetVariationForFeatureExperimentGivenNullExperimentIds()
         {
             var featureFlag = ProjectConfig.GetFeatureFlagFromKey("empty_feature");
-            var decision = DecisionService.GetVariationForFeatureExperiment(featureFlag, GenericUserId, new UserAttributes() { }, ProjectConfig, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance());
+            var decision = DecisionService.GetVariationForFeatureExperiment(featureFlag, GenericUserId, new UserAttributes() { }, ProjectConfig, new OptimizelyDecideOption[]{}, DefaultDecisionReasons.NewInstance());
 
             Assert.IsNull(decision);
 
@@ -488,7 +488,7 @@ public void TestGetVariationForFeatureExperimentGivenExperimentNotInDataFile()
                 ExperimentIds = new List<string> { "29039203" }
             };
 
-            var decision = DecisionService.GetVariationForFeatureExperiment(featureFlag, GenericUserId, new UserAttributes() { }, ProjectConfig, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance());
+            var decision = DecisionService.GetVariationForFeatureExperiment(featureFlag, GenericUserId, new UserAttributes() { }, ProjectConfig, new OptimizelyDecideOption[] { }, DefaultDecisionReasons.NewInstance());
             Assert.IsNull(decision);
 
             LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Experiment ID \"29039203\" is not in datafile."));
@@ -503,7 +503,7 @@ public void TestGetVariationForFeatureExperimentGivenNonMutexGroupAndUserNotBuck
             DecisionServiceMock.Setup(ds => ds.GetVariation(multiVariateExp, "user1", ProjectConfig, null)).Returns<Variation>(null);
             var featureFlag = ProjectConfig.GetFeatureFlagFromKey("multi_variate_feature");
 
-            var decision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", new UserAttributes(), ProjectConfig, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance());
+            var decision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", new UserAttributes(), ProjectConfig, new OptimizelyDecideOption[] { }, DefaultDecisionReasons.NewInstance());
             Assert.IsNull(decision);
 
             LoggerMock.Verify(l => l.Log(LogLevel.INFO, "The user \"user1\" is not bucketed into any of the experiments on the feature \"multi_variate_feature\"."));
@@ -519,10 +519,10 @@ public void TestGetVariationForFeatureExperimentGivenNonMutexGroupAndUserIsBucke
             var userAttributes = new UserAttributes();
 
             DecisionServiceMock.Setup(ds => ds.GetVariation(ProjectConfig.GetExperimentFromKey("test_experiment_multivariate"),
-                "user1", ProjectConfig, userAttributes, It.IsAny<List<OptimizelyDecideOption>>(), It.IsAny<IDecisionReasons>())).Returns(variation);
+                "user1", ProjectConfig, userAttributes, It.IsAny<OptimizelyDecideOption[]> (), It.IsAny<IDecisionReasons>())).Returns(variation);
 
             var featureFlag = ProjectConfig.GetFeatureFlagFromKey("multi_variate_feature");
-            var decision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance());
+            var decision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig, new OptimizelyDecideOption[] { }, DefaultDecisionReasons.NewInstance());
 
             Assert.IsTrue(TestData.CompareObjects(expectedDecision, decision));
 
@@ -542,7 +542,7 @@ public void TestGetVariationForFeatureExperimentGivenMutexGroupAndUserIsBucketed
                 userAttributes)).Returns(variation);
 
             var featureFlag = ProjectConfig.GetFeatureFlagFromKey("boolean_feature");
-            var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance());
+            var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig, new OptimizelyDecideOption[] { }, DefaultDecisionReasons.NewInstance());
 
             Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision));
 
@@ -554,11 +554,11 @@ public void TestGetVariationForFeatureExperimentGivenMutexGroupAndUserIsBucketed
         public void TestGetVariationForFeatureExperimentGivenMutexGroupAndUserNotBucketed()
         {
             var mutexExperiment = ProjectConfig.GetExperimentFromKey("group_experiment_1");
-            DecisionServiceMock.Setup(ds => ds.GetVariation(It.IsAny<Experiment>(), It.IsAny<string>(), ProjectConfig, It.IsAny<UserAttributes>(), It.IsAny<List<OptimizelyDecideOption>>(), It.IsAny<IDecisionReasons>())).
+            DecisionServiceMock.Setup(ds => ds.GetVariation(It.IsAny<Experiment>(), It.IsAny<string>(), ProjectConfig, It.IsAny<UserAttributes>(), It.IsAny<OptimizelyDecideOption[]> (), It.IsAny<IDecisionReasons>())).
                 Returns<Variation>(null);
 
             var featureFlag = ProjectConfig.GetFeatureFlagFromKey("boolean_feature");
-            var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", new UserAttributes(), ProjectConfig, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance());
+            var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", new UserAttributes(), ProjectConfig, new OptimizelyDecideOption[] { }, DefaultDecisionReasons.NewInstance());
 
             Assert.IsNull(actualDecision);
 
@@ -601,7 +601,7 @@ public void TestGetVariationForFeatureRolloutWhenRolloutIsNotInDataFile()
                 Variables = featureFlag.Variables
             };
 
-            DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny<FeatureFlag>(), It.IsAny<string>(), It.IsAny<UserAttributes>(), ProjectConfig, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance())).Returns<Variation>(null);
+            DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny<FeatureFlag>(), It.IsAny<string>(), It.IsAny<UserAttributes>(), ProjectConfig, new OptimizelyDecideOption[] { }, DefaultDecisionReasons.NewInstance())).Returns<Variation>(null);
 
             var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureRollout(featureFlag, "user1", new UserAttributes(), ProjectConfig, DefaultDecisionReasons.NewInstance());
             Assert.IsNull(actualDecision);
@@ -808,7 +808,7 @@ public void TestGetVariationForFeatureWhenTheUserIsBucketedIntoFeatureExperiment
             var expectedDecision = new FeatureDecision(expectedExperiment, variation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST);
 
             DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny<FeatureFlag>(), It.IsAny<string>(),
-                It.IsAny<UserAttributes>(), ProjectConfig, It.IsAny<List<OptimizelyDecideOption>>(), It.IsAny<IDecisionReasons>())).Returns(expectedDecision);
+                It.IsAny<UserAttributes>(), ProjectConfig, It.IsAny<OptimizelyDecideOption[]>(), It.IsAny<IDecisionReasons>())).Returns(expectedDecision);
 
             var actualDecision = DecisionServiceMock.Object.GetVariationForFeature(featureFlag, "user1", ProjectConfig, new UserAttributes());
             Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision));
@@ -827,7 +827,7 @@ public void TestGetVariationForFeatureWhenTheUserIsNotBucketedIntoFeatureExperim
             var expectedDecision = new FeatureDecision(expectedExperiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT);
 
             DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny<FeatureFlag>(), It.IsAny<string>(),
-                It.IsAny<UserAttributes>(), ProjectConfig, It.IsAny<List<OptimizelyDecideOption>>(), It.IsAny<IDecisionReasons>())).Returns<Variation>(null);
+                It.IsAny<UserAttributes>(), ProjectConfig, It.IsAny<OptimizelyDecideOption[]>(), It.IsAny<IDecisionReasons>())).Returns<Variation>(null);
             DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureRollout(It.IsAny<FeatureFlag>(), It.IsAny<string>(),
                 It.IsAny<UserAttributes>(), ProjectConfig, It.IsAny<IDecisionReasons>())).Returns(expectedDecision);
 
@@ -845,7 +845,7 @@ public void TestGetVariationForFeatureWhenTheUserIsNeitherBucketedIntoFeatureExp
             var featureFlag = ProjectConfig.GetFeatureFlagFromKey("string_single_variable_feature");
             var expectedDecision = new FeatureDecision(null, null, FeatureDecision.DECISION_SOURCE_ROLLOUT);
 
-            DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny<FeatureFlag>(), It.IsAny<string>(), It.IsAny<UserAttributes>(), ProjectConfig, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance())).Returns<Variation>(null);
+            DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny<FeatureFlag>(), It.IsAny<string>(), It.IsAny<UserAttributes>(), ProjectConfig, new OptimizelyDecideOption[] { }, DefaultDecisionReasons.NewInstance())).Returns<Variation>(null);
             DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureRollout(It.IsAny<FeatureFlag>(), It.IsAny<string>(), It.IsAny<UserAttributes>(), ProjectConfig, DefaultDecisionReasons.NewInstance())).Returns<Variation>(null);
 
             var actualDecision = DecisionServiceMock.Object.GetVariationForFeature(featureFlag, "user1", ProjectConfig, new UserAttributes());
@@ -867,8 +867,8 @@ public void TestGetVariationForFeatureWhenTheUserIsBuckedtedInBothExperimentAndR
                 { "browser_type", "chrome" }
             };
 
-            DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, "user1", ProjectConfig, userAttributes, It.IsAny<List<OptimizelyDecideOption>>(), It.IsAny<IDecisionReasons>())).Returns(variation);
-            var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance());
+            DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, "user1", ProjectConfig, userAttributes, It.IsAny<OptimizelyDecideOption[]>(), It.IsAny<IDecisionReasons>())).Returns(variation);
+            var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig, new OptimizelyDecideOption[] { }, DefaultDecisionReasons.NewInstance());
 
             // The user is bucketed into feature experiment's variation.
             Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision));
diff --git a/OptimizelySDK.Tests/OptimizelyDecisions/OptimizelyDecisionTest.cs b/OptimizelySDK.Tests/OptimizelyDecisions/OptimizelyDecisionTest.cs
index bf8f5f17..d5248e7c 100644
--- a/OptimizelySDK.Tests/OptimizelyDecisions/OptimizelyDecisionTest.cs
+++ b/OptimizelySDK.Tests/OptimizelyDecisions/OptimizelyDecisionTest.cs
@@ -84,7 +84,7 @@ public void TestNewDecision()
         [Test]
         public void TestNewDecisionReasonWithIncludeReasons()
         {
-            var decisionReasons = DefaultDecisionReasons.NewInstance(new List<OptimizelyDecideOption>() { OptimizelyDecideOption.INCLUDE_REASONS });
+            var decisionReasons = DefaultDecisionReasons.NewInstance(new OptimizelyDecideOption[] { OptimizelyDecideOption.INCLUDE_REASONS });
             decisionReasons.AddError(DecisionMessage.Reason(DecisionMessage.FLAG_KEY_INVALID, "invalid_key"));
             
             Assert.AreEqual(decisionReasons.ToReport()[0], "No flag was found for key \"invalid_key\".");
diff --git a/OptimizelySDK/Bucketing/DecisionService.cs b/OptimizelySDK/Bucketing/DecisionService.cs
index c0ffd46c..68564470 100644
--- a/OptimizelySDK/Bucketing/DecisionService.cs
+++ b/OptimizelySDK/Bucketing/DecisionService.cs
@@ -89,7 +89,7 @@ public virtual Variation GetVariation(Experiment experiment,
             ProjectConfig config,
             UserAttributes filteredAttributes)
         {
-            return GetVariation(experiment, userId, config, filteredAttributes, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance());
+            return GetVariation(experiment, userId, config, filteredAttributes, new OptimizelyDecideOption[] { }, DefaultDecisionReasons.NewInstance());
         }
 
         /// <summary>
@@ -103,7 +103,7 @@ public virtual Variation GetVariation(Experiment experiment,
             string userId,
             ProjectConfig config,
             UserAttributes filteredAttributes,
-            List<OptimizelyDecideOption> options,
+            OptimizelyDecideOption[] options,
             IDecisionReasons reasons)
         {
             if (!ExperimentUtils.IsExperimentActive(experiment, Logger)) return null;
@@ -117,7 +117,7 @@ public virtual Variation GetVariation(Experiment experiment,
 
             if (variation != null) return variation;
             // fetch the user profile map from the user profile service
-            var ignoreUPS = options.Contains(OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE);
+            var ignoreUPS = Array.Exists(options, option => option == OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE);
 
             UserProfile userProfile = null;
             if (UserProfileService != null && !ignoreUPS)
@@ -515,7 +515,7 @@ public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag feat
             string userId,
             UserAttributes filteredAttributes,
             ProjectConfig config,
-            List<OptimizelyDecideOption> options,
+            OptimizelyDecideOption[] options,
             IDecisionReasons reasons)
         {
             if (featureFlag == null)
@@ -560,7 +560,7 @@ public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag feat
         /// successfully bucketed.</returns>
         public virtual FeatureDecision GetVariationForFeature(FeatureFlag featureFlag, string userId, ProjectConfig config, UserAttributes filteredAttributes)
         {
-            return GetVariationForFeature(featureFlag, userId, config, filteredAttributes, new List<OptimizelyDecideOption>(), DefaultDecisionReasons.NewInstance());
+            return GetVariationForFeature(featureFlag, userId, config, filteredAttributes, new OptimizelyDecideOption[] { }, DefaultDecisionReasons.NewInstance());
         }
 
         /// <summary>
@@ -578,7 +578,7 @@ public virtual FeatureDecision GetVariationForFeature(FeatureFlag featureFlag,
             string userId,
             ProjectConfig config,
             UserAttributes filteredAttributes,
-            List<OptimizelyDecideOption> options, 
+            OptimizelyDecideOption[] options, 
             IDecisionReasons reasons)
         {
             // Check if the feature flag has an experiment and the user is bucketed into that experiment.
diff --git a/OptimizelySDK/Optimizely.cs b/OptimizelySDK/Optimizely.cs
index 0b7209ed..e36a6ec4 100644
--- a/OptimizelySDK/Optimizely.cs
+++ b/OptimizelySDK/Optimizely.cs
@@ -30,6 +30,7 @@
 using OptimizelySDK.OptlyConfig;
 using System.Net;
 using OptimizelySDK.OptimizelyDecisions;
+using System.Linq;
 
 namespace OptimizelySDK
 {
@@ -721,9 +722,9 @@ public OptimizelyUserContext CreateUserContext(string userId,
         /// <param name="key">A flag key for which a decision will be made.</param>
         /// <param name="options">A list of options for decision-making.</param>
         /// <returns>A decision result.</returns>
-        public OptimizelyDecision Decide(OptimizelyUserContext user,
+        internal OptimizelyDecision Decide(OptimizelyUserContext user,
                               string key,
-                              List<OptimizelyDecideOption> options)
+                              OptimizelyDecideOption[] options)
         {
 
             var config = ProjectConfigManager?.GetConfig();
@@ -835,12 +836,13 @@ public OptimizelyDecision Decide(OptimizelyUserContext user,
                 reasonsToReport.ToArray());
         }
 
-        private List<OptimizelyDecideOption> GetAllOptions(List<OptimizelyDecideOption> options)
+        private OptimizelyDecideOption[] GetAllOptions(OptimizelyDecideOption[] options)
         {
-            var copiedOptions = new List<OptimizelyDecideOption>(DefaultDecideOptions);
+            OptimizelyDecideOption[] copiedOptions = new OptimizelyDecideOption[DefaultDecideOptions.Length];
+            Array.Copy(DefaultDecideOptions, copiedOptions, DefaultDecideOptions.Length);
             if (options != null)
             {
-                copiedOptions.AddRange(options);
+                copiedOptions.Concat(options);
             }
             return copiedOptions;
         }
diff --git a/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs b/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs
index 0ded3470..e02aa82d 100644
--- a/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs
+++ b/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs
@@ -14,6 +14,7 @@
  * limitations under the License.
  */
 
+using System;
 using System.Collections.Generic;
 
 namespace OptimizelySDK.OptimizelyDecisions
@@ -23,9 +24,9 @@ public class DefaultDecisionReasons : IDecisionReasons
         private List<string> Errors = new List<string>();
         private List<string> Infos = new List<string>();
 
-        public static IDecisionReasons NewInstance(List<OptimizelyDecideOption> options)
+        public static IDecisionReasons NewInstance(OptimizelyDecideOption[] options)
         {
-            if (options != null && options.Contains(OptimizelyDecideOption.INCLUDE_REASONS))
+            if (options != null && Array.Exists(options, option => option == OptimizelyDecideOption.INCLUDE_REASONS))
             {
                 return new DefaultDecisionReasons();
             }
diff --git a/OptimizelySDK/OptimizelyUserContext.cs b/OptimizelySDK/OptimizelyUserContext.cs
index 32d035af..8f76c7a7 100644
--- a/OptimizelySDK/OptimizelyUserContext.cs
+++ b/OptimizelySDK/OptimizelyUserContext.cs
@@ -73,7 +73,7 @@ public void SetAttribute(string key, object value)
         /// <returns>A decision result.</returns>
         public OptimizelyDecision Decide(string key)
         {
-            return Decide(key, new List<OptimizelyDecideOption>());
+            return Decide(key, new OptimizelyDecideOption[] { });
         }
 
         /// <summary>
@@ -86,7 +86,7 @@ public OptimizelyDecision Decide(string key)
         /// <param name="options">A list of options for decision-making.</param>
         /// <returns>A decision result.</returns>
         public OptimizelyDecision Decide(string key,
-            List<OptimizelyDecideOption> options)
+            OptimizelyDecideOption[] options)
         {
             return Optimizely.Decide(this, key, options);
         }
@@ -107,7 +107,7 @@ public Dictionary<string, OptimizelyDecision> DecideForKeys(List<string> keys)
         /// <returns>A dictionary of all decision results, mapped by flag keys.</returns>
         public Dictionary<string, OptimizelyDecision> DecideAll()
         {
-            return DecideAll(new List<OptimizelyDecideOption>());
+            return DecideAll(new OptimizelyDecideOption[] { });
         }
 
 
@@ -116,7 +116,7 @@ public Dictionary<string, OptimizelyDecision> DecideAll()
         /// </summary>
         /// <param name="options">A list of options for decision-making.</param>
         /// <returns>All decision results mapped by flag keys.</returns>
-        public Dictionary<string, OptimizelyDecision> DecideAll(List<OptimizelyDecideOption> options)
+        public Dictionary<string, OptimizelyDecision> DecideAll(OptimizelyDecideOption[] options)
         {
             throw new NotImplementedException();
         }

From 54e9e747ed33462f7d142c8d1d2f6be529dd7891 Mon Sep 17 00:00:00 2001
From: Muhammad Noman <muhammadnoman@folio3.com>
Date: Thu, 3 Dec 2020 03:59:25 +0500
Subject: [PATCH 17/19] Removed unnecessary methods from errorDecisionsReasons
 removed static from mutex object

---
 .../OptimizelyDecisions/DefaultDecisionReasons.cs |  6 +++---
 .../OptimizelyDecisions/ErrorsDecisionReasons.cs  | 15 ++-------------
 OptimizelySDK/OptimizelyUserContext.cs            |  2 +-
 3 files changed, 6 insertions(+), 17 deletions(-)

diff --git a/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs b/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs
index 0ded3470..2bce30c2 100644
--- a/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs
+++ b/OptimizelySDK/OptimizelyDecisions/DefaultDecisionReasons.cs
@@ -20,7 +20,7 @@ namespace OptimizelySDK.OptimizelyDecisions
 {
     public class DefaultDecisionReasons : IDecisionReasons
     {
-        private List<string> Errors = new List<string>();
+        protected List<string> Errors = new List<string>();
         private List<string> Infos = new List<string>();
 
         public static IDecisionReasons NewInstance(List<OptimizelyDecideOption> options)
@@ -46,14 +46,14 @@ public void AddError(string format, params object[] args)
             Errors.Add(message);
         }
 
-        public string AddInfo(string format, params object[] args)
+        public virtual string AddInfo(string format, params object[] args)
         {
             string message = string.Format(format, args);
             Infos.Add(message);
             return message;
         }
 
-        public List<string> ToReport()
+        public virtual List<string> ToReport()
         {
             List<string> reasons = new List<string>(Errors);
             reasons.AddRange(Infos);
diff --git a/OptimizelySDK/OptimizelyDecisions/ErrorsDecisionReasons.cs b/OptimizelySDK/OptimizelyDecisions/ErrorsDecisionReasons.cs
index 56e98d7e..2c0b9348 100644
--- a/OptimizelySDK/OptimizelyDecisions/ErrorsDecisionReasons.cs
+++ b/OptimizelySDK/OptimizelyDecisions/ErrorsDecisionReasons.cs
@@ -21,25 +21,14 @@ namespace OptimizelySDK.OptimizelyDecisions
     /// <summary>
     /// NewErrorDecision returns a decision with errors only
     /// </summary>
-    public class ErrorsDecisionReasons : IDecisionReasons
+    public class ErrorsDecisionReasons : DefaultDecisionReasons
     {
-        private readonly List<string> errors = new List<string>();
 
-        public void AddError(string format, params object[] args)
-        {
-            string message = string.Format(format, args);
-            errors.Add(message);
-        }
-
-        public string AddInfo(string format, params object[] args)
+        public override string AddInfo(string format, params object[] args)
         {
             // skip tracking and pass-through reasons other than critical errors.
             return string.Format(format, args);
         }
 
-        public List<string> ToReport()
-        {
-            return errors;
-        }
     }
 }
diff --git a/OptimizelySDK/OptimizelyUserContext.cs b/OptimizelySDK/OptimizelyUserContext.cs
index e273c7c7..ba53f747 100644
--- a/OptimizelySDK/OptimizelyUserContext.cs
+++ b/OptimizelySDK/OptimizelyUserContext.cs
@@ -30,7 +30,7 @@ public class OptimizelyUserContext
     {
         private ILogger Logger;
         private IErrorHandler ErrorHandler;
-        private static object mutex = new object();
+        private object mutex = new object();
         // userID for Optimizely user context
         public string UserId { get; }
         // user attributes for Optimizely user context.

From b824b990beb840c61d8c549bfbd7a518122a85fc Mon Sep 17 00:00:00 2001
From: Muhammad Noman <muhammadnoman@folio3.com>
Date: Thu, 3 Dec 2020 04:19:18 +0500
Subject: [PATCH 18/19] Added ignoreUps check first in decision service added
 check of decisionReason in test that it should be zero if no error log occurs

---
 OptimizelySDK.Tests/BucketerTest.cs           | 36 +++++++++++--------
 .../OptimizelyUserContextTest.cs              |  2 --
 OptimizelySDK/Bucketing/DecisionService.cs    |  2 +-
 3 files changed, 22 insertions(+), 18 deletions(-)

diff --git a/OptimizelySDK.Tests/BucketerTest.cs b/OptimizelySDK.Tests/BucketerTest.cs
index 1bf4548b..8aa94d0b 100644
--- a/OptimizelySDK.Tests/BucketerTest.cs
+++ b/OptimizelySDK.Tests/BucketerTest.cs
@@ -28,6 +28,7 @@ public class BucketerTest
     {
         private Mock<ILogger> LoggerMock;
         private ProjectConfig Config;
+        private IDecisionReasons DecisionReasons;
         private const string TestUserId = "testUserId";
         public string TestBucketingIdControl { get; } = "testBucketingIdControl!"; // generates bucketing number 3741
         public string TestBucketingIdVariation { get; } = "123456789'"; // generates bucketing number 4567
@@ -60,6 +61,7 @@ public override string ToString()
         public void Initialize()
         {
             LoggerMock = new Mock<ILogger>();
+            DecisionReasons = DefaultDecisionReasons.NewInstance();
             Config = DatafileProjectConfig.Create(TestData.Datafile, LoggerMock.Object, new ErrorHandler.NoOpErrorHandler());
         }
 
@@ -96,18 +98,16 @@ public void TestBucketValidExperimentNotInGroup()
         {
             TestBucketer bucketer = new TestBucketer(LoggerMock.Object);
             bucketer.SetBucketValues(new[] { 3000, 7000, 9000 });
-
             // control
             Assert.AreEqual(new Variation { Id = "7722370027", Key = "control" },
-                bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance()));
+                bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId, DecisionReasons));
 
             LoggerMock.Verify(l => l.Log(It.IsAny<LogLevel>(), It.IsAny<string>()), Times.Exactly(2));
             LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [3000] to user [testUserId] with bucketing ID [testBucketingIdControl!]."));
             LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [testUserId] is in variation [control] of experiment [test_experiment]."));
-
             // variation
             Assert.AreEqual(new Variation { Id = "7721010009", Key = "variation" },
-                bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance()));
+                bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId, DecisionReasons));
 
             LoggerMock.Verify(l => l.Log(It.IsAny<LogLevel>(), It.IsAny<string>()), Times.Exactly(4));
             LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [7000] to user [testUserId] with bucketing ID [testBucketingIdControl!]."));
@@ -115,23 +115,24 @@ public void TestBucketValidExperimentNotInGroup()
 
             // no variation
             Assert.AreEqual(new Variation { },
-                bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance()));
+                bucketer.Bucket(Config, Config.GetExperimentFromKey("test_experiment"), TestBucketingIdControl, TestUserId, DecisionReasons));
 
             LoggerMock.Verify(l => l.Log(It.IsAny<LogLevel>(), It.IsAny<string>()), Times.Exactly(6));
             LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [9000] to user [testUserId] with bucketing ID [testBucketingIdControl!]."));
             LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [testUserId] is in no variation."));
+            Assert.AreEqual(DecisionReasons.ToReport().Count, 0);
         }
 
         [Test]
         public void TestBucketValidExperimentInGroup()
         {
             TestBucketer bucketer = new TestBucketer(LoggerMock.Object);
-
+            
             // group_experiment_1 (20% experiment)
             // variation 1
             bucketer.SetBucketValues(new[] { 1000, 4000 });
             Assert.AreEqual(new Variation { Id = "7722260071", Key = "group_exp_1_var_1" },
-                bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance()));
+                bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId, DecisionReasons));
             LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [1000] to user [testUserId] with bucketing ID [testBucketingIdControl!]."));
             LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [testUserId] is in experiment [group_experiment_1] of group [7722400015]."));
             LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [4000] to user [testUserId] with bucketing ID [testBucketingIdControl!]."));
@@ -140,7 +141,7 @@ public void TestBucketValidExperimentInGroup()
             // variation 2
             bucketer.SetBucketValues(new[] { 1500, 7000 });
             Assert.AreEqual(new Variation { Id = "7722360022", Key = "group_exp_1_var_2" },
-                bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance()));
+                bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId, DecisionReasons));
             LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [1500] to user [testUserId] with bucketing ID [testBucketingIdControl!]."));
             LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [testUserId] is in experiment [group_experiment_1] of group [7722400015]."));
             LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [7000] to user [testUserId] with bucketing ID [testBucketingIdControl!]."));
@@ -149,20 +150,21 @@ public void TestBucketValidExperimentInGroup()
             // User not in experiment
             bucketer.SetBucketValues(new[] { 5000, 7000 });
             Assert.AreEqual(new Variation { },
-                bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance()));
+                bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_1"), TestBucketingIdControl, TestUserId, DecisionReasons));
             LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [5000] to user [testUserId] with bucketing ID [testBucketingIdControl!]."));
             LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [testUserId] is not in experiment [group_experiment_1] of group [7722400015]."));
 
             LoggerMock.Verify(l => l.Log(It.IsAny<LogLevel>(), It.IsAny<string>()), Times.Exactly(10));
+            Assert.AreEqual(DecisionReasons.ToReport().Count, 0);
         }
 
         [Test]
         public void TestBucketInvalidExperiment()
         {
             var bucketer = new Bucketer(LoggerMock.Object);
-
+            
             Assert.AreEqual(new Variation { },
-                bucketer.Bucket(Config, new Experiment(), TestBucketingIdControl, TestUserId, DefaultDecisionReasons.NewInstance()));
+                bucketer.Bucket(Config, new Experiment(), TestBucketingIdControl, TestUserId, DecisionReasons));
 
             LoggerMock.Verify(l => l.Log(It.IsAny<LogLevel>(), It.IsAny<string>()), Times.Never);
         }
@@ -177,7 +179,8 @@ public void TestBucketWithBucketingId()
 
             // make sure that the bucketing ID is used for the variation bucketing and not the user ID
             Assert.AreEqual(expectedVariation, 
-                bucketer.Bucket(Config, experiment, TestBucketingIdControl, TestUserIdBucketsToVariation, DefaultDecisionReasons.NewInstance()));
+                bucketer.Bucket(Config, experiment, TestBucketingIdControl, TestUserIdBucketsToVariation, DecisionReasons));
+            Assert.AreEqual(DecisionReasons.ToReport().Count, 0);
         }
 
         // Test for invalid experiment keys, null variation should be returned
@@ -188,7 +191,8 @@ public void TestBucketVariationInvalidExperimentsWithBucketingId()
             var expectedVariation = new Variation();
 
             Assert.AreEqual(expectedVariation, 
-                bucketer.Bucket(Config, Config.GetExperimentFromKey("invalid_experiment"), TestBucketingIdVariation, TestUserId, DefaultDecisionReasons.NewInstance()));
+                bucketer.Bucket(Config, Config.GetExperimentFromKey("invalid_experiment"), TestBucketingIdVariation, TestUserId, DecisionReasons));
+            Assert.AreEqual(DecisionReasons.ToReport().Count, 0);
         }
 
         // Make sure that the bucketing ID is used to bucket the user into a group and not the user ID
@@ -201,7 +205,8 @@ public void TestBucketVariationGroupedExperimentsWithBucketingId()
 
             Assert.AreEqual(expectedGroupVariation,
                 bucketer.Bucket(Config, Config.GetExperimentFromKey("group_experiment_2"), 
-                TestBucketingIdGroupExp2Var2, TestUserIdBucketsToNoGroup, DefaultDecisionReasons.NewInstance()));
+                TestBucketingIdGroupExp2Var2, TestUserIdBucketsToNoGroup, DecisionReasons));
+            Assert.AreEqual(DecisionReasons.ToReport().Count, 0);
         }
 
         // Make sure that user gets bucketed into the rollout rule.
@@ -214,7 +219,8 @@ public void TestBucketRolloutRule()
             var expectedVariation = Config.GetVariationFromId(rolloutRule.Key, "177773");
 
             Assert.True(TestData.CompareObjects(expectedVariation, 
-                bucketer.Bucket(Config, rolloutRule, "testBucketingId", TestUserId, DefaultDecisionReasons.NewInstance())));
+                bucketer.Bucket(Config, rolloutRule, "testBucketingId", TestUserId, DecisionReasons)));
+            Assert.AreEqual(DecisionReasons.ToReport().Count, 0);
         }
     }
 }
diff --git a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs
index 1238e73e..cd976a54 100644
--- a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs
+++ b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs
@@ -15,7 +15,6 @@
  *    limitations under the License.
  */
 
-using Castle.Core.Internal;
 using Moq;
 using NUnit.Framework;
 using OptimizelySDK.Entity;
@@ -24,7 +23,6 @@
 using OptimizelySDK.Logger;
 using OptimizelySDK.OptimizelyDecisions;
 using System;
-using System.Collections.Generic;
 
 namespace OptimizelySDK.Tests
 {
diff --git a/OptimizelySDK/Bucketing/DecisionService.cs b/OptimizelySDK/Bucketing/DecisionService.cs
index 68564470..8b6963b4 100644
--- a/OptimizelySDK/Bucketing/DecisionService.cs
+++ b/OptimizelySDK/Bucketing/DecisionService.cs
@@ -120,7 +120,7 @@ public virtual Variation GetVariation(Experiment experiment,
             var ignoreUPS = Array.Exists(options, option => option == OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE);
 
             UserProfile userProfile = null;
-            if (UserProfileService != null && !ignoreUPS)
+            if (!ignoreUPS && UserProfileService != null)
             {
                 try
                 {

From eda27436b6605bda24967f4ed3d597f38ed066a3 Mon Sep 17 00:00:00 2001
From: Muhammad Noman <muhammadnoman@folio3.com>
Date: Mon, 7 Dec 2020 17:39:47 +0500
Subject: [PATCH 19/19] Nit Fix

---
 OptimizelySDK.Tests/OptimizelyUserContextTest.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs
index cd976a54..08fb9a34 100644
--- a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs
+++ b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs
@@ -169,7 +169,7 @@ public void TestDecide()
             Assert.AreEqual(decision.RuleKey, "test_experiment_multivariate");
             Assert.AreEqual(decision.FlagKey, flagKey);
             Assert.AreEqual(decision.UserContext, user);
-            Assert.IsNotNull(decision.Reasons);
+            Assert.AreEqual(decision.Reasons.Length, 0);
         }
 
         [Test]