Skip to content

Commit c943f08

Browse files
[FSSDK-11168] cmab service impl
1 parent 34c736c commit c943f08

File tree

4 files changed

+223
-0
lines changed

4 files changed

+223
-0
lines changed
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
/*
2+
* Copyright 2025, Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
using System;
18+
using System.Collections.Generic;
19+
using System.Linq;
20+
using System.Security.Cryptography;
21+
using System.Text;
22+
using Newtonsoft.Json;
23+
using OptimizelySDK;
24+
using OptimizelySDK.Entity;
25+
using OptimizelySDK.Logger;
26+
using OptimizelySDK.Odp;
27+
using OptimizelySDK.OptimizelyDecisions;
28+
using AttributeEntity = OptimizelySDK.Entity.Attribute;
29+
30+
namespace OptimizelySDK.Cmab
31+
{
32+
/// <summary>
33+
/// Represents a CMAB decision response returned by the service.
34+
/// </summary>
35+
public class CmabDecision
36+
{
37+
public CmabDecision(string variationId, string cmabUuid)
38+
{
39+
VariationId = variationId;
40+
CmabUuid = cmabUuid;
41+
}
42+
43+
public string VariationId { get; }
44+
public string CmabUuid { get; }
45+
}
46+
47+
public class CmabCacheEntry
48+
{
49+
public string AttributesHash { get; set; }
50+
public string VariationId { get; set; }
51+
public string CmabUuid { get; set; }
52+
}
53+
54+
/// <summary>
55+
/// Default implementation of the CMAB decision service that handles caching and filtering.
56+
/// </summary>
57+
public class DefaultCmabService : ICmabService
58+
{
59+
private readonly LruCache<CmabCacheEntry> _cmabCache;
60+
private readonly ICmabClient _cmabClient;
61+
private readonly ILogger _logger;
62+
63+
public DefaultCmabService(LruCache<CmabCacheEntry> cmabCache,
64+
ICmabClient cmabClient,
65+
ILogger logger = null)
66+
{
67+
_cmabCache = cmabCache;
68+
_cmabClient = cmabClient;
69+
_logger = logger ?? new NoOpLogger();
70+
}
71+
72+
public CmabDecision GetDecision(ProjectConfig projectConfig,
73+
OptimizelyUserContext userContext,
74+
string ruleId,
75+
OptimizelyDecideOption[] options)
76+
{
77+
var optionSet = options ?? new OptimizelyDecideOption[0];
78+
var filteredAttributes = FilterAttributes(projectConfig, userContext, ruleId);
79+
80+
if (optionSet.Contains(OptimizelyDecideOption.IGNORE_CMAB_CACHE))
81+
{
82+
return FetchDecision(ruleId, userContext.GetUserId(), filteredAttributes);
83+
}
84+
85+
if (optionSet.Contains(OptimizelyDecideOption.RESET_CMAB_CACHE))
86+
{
87+
_cmabCache.Reset();
88+
}
89+
90+
var cacheKey = GetCacheKey(userContext.GetUserId(), ruleId);
91+
92+
if (optionSet.Contains(OptimizelyDecideOption.INVALIDATE_USER_CMAB_CACHE))
93+
{
94+
_cmabCache.Remove(cacheKey);
95+
}
96+
97+
var cachedValue = _cmabCache.Lookup(cacheKey);
98+
var attributesHash = HashAttributes(filteredAttributes);
99+
100+
if (cachedValue != null)
101+
{
102+
if (string.Equals(cachedValue.AttributesHash, attributesHash, StringComparison.Ordinal))
103+
{
104+
return new CmabDecision(cachedValue.VariationId, cachedValue.CmabUuid);
105+
}
106+
107+
_cmabCache.Remove(cacheKey);
108+
}
109+
110+
var cmabDecision = FetchDecision(ruleId, userContext.GetUserId(), filteredAttributes);
111+
112+
_cmabCache.Save(cacheKey, new CmabCacheEntry
113+
{
114+
AttributesHash = attributesHash,
115+
VariationId = cmabDecision.VariationId,
116+
CmabUuid = cmabDecision.CmabUuid,
117+
});
118+
119+
return cmabDecision;
120+
}
121+
122+
private CmabDecision FetchDecision(string ruleId,
123+
string userId,
124+
UserAttributes attributes)
125+
{
126+
var cmabUuid = Guid.NewGuid().ToString();
127+
var variationId = _cmabClient.FetchDecision(ruleId, userId, attributes, cmabUuid);
128+
return new CmabDecision(variationId, cmabUuid);
129+
}
130+
131+
private UserAttributes FilterAttributes(ProjectConfig projectConfig,
132+
OptimizelyUserContext userContext,
133+
string ruleId)
134+
{
135+
var filtered = new UserAttributes();
136+
137+
if (projectConfig.ExperimentIdMap == null ||
138+
!projectConfig.ExperimentIdMap.TryGetValue(ruleId, out var experiment) ||
139+
experiment?.Cmab?.AttributeIds == null ||
140+
experiment.Cmab.AttributeIds.Count == 0)
141+
{
142+
return filtered;
143+
}
144+
145+
var userAttributes = userContext.GetAttributes() ?? new UserAttributes();
146+
var attributeIdMap = projectConfig.AttributeIdMap ?? new Dictionary<string, AttributeEntity>();
147+
148+
foreach (var attributeId in experiment.Cmab.AttributeIds)
149+
{
150+
if (attributeIdMap.TryGetValue(attributeId, out AttributeEntity attribute) &&
151+
attribute != null &&
152+
!string.IsNullOrEmpty(attribute.Key) &&
153+
userAttributes.TryGetValue(attribute.Key, out var value))
154+
{
155+
filtered[attribute.Key] = value;
156+
}
157+
}
158+
159+
return filtered;
160+
}
161+
162+
private string GetCacheKey(string userId, string ruleId)
163+
{
164+
var normalizedUserId = userId ?? string.Empty;
165+
return $"{normalizedUserId.Length}-{normalizedUserId}-{ruleId}";
166+
}
167+
168+
private string HashAttributes(UserAttributes attributes)
169+
{
170+
var ordered = attributes.OrderBy(kvp => kvp.Key)
171+
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
172+
var serialized = JsonConvert.SerializeObject(ordered);
173+
174+
using (var md5 = MD5.Create())
175+
{
176+
var hashBytes = md5.ComputeHash(Encoding.UTF8.GetBytes(serialized));
177+
var builder = new StringBuilder(hashBytes.Length * 2);
178+
foreach (var b in hashBytes)
179+
{
180+
builder.Append(b.ToString("x2"));
181+
}
182+
183+
return builder.ToString();
184+
}
185+
}
186+
}
187+
}

OptimizelySDK/Cmab/ICmabService.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright 2025, Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
using OptimizelySDK.OptimizelyDecisions;
18+
19+
namespace OptimizelySDK.Cmab
20+
{
21+
/// <summary>
22+
/// Contract for CMAB decision services.
23+
/// </summary>
24+
public interface ICmabService
25+
{
26+
CmabDecision GetDecision(ProjectConfig projectConfig,
27+
OptimizelyUserContext userContext,
28+
string ruleId,
29+
OptimizelyDecideOption[] options);
30+
}
31+
}

OptimizelySDK/OptimizelyDecisions/OptimizelyDecideOption.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,8 @@ public enum OptimizelyDecideOption
2323
IGNORE_USER_PROFILE_SERVICE,
2424
INCLUDE_REASONS,
2525
EXCLUDE_VARIABLES,
26+
IGNORE_CMAB_CACHE,
27+
RESET_CMAB_CACHE,
28+
INVALIDATE_USER_CMAB_CACHE,
2629
}
2730
}

OptimizelySDK/OptimizelySDK.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,9 @@
205205
<Compile Include="Entity\Result.cs"/>
206206
<Compile Include="Notifications\NotificationCenterRegistry.cs"/>
207207
<Compile Include="Cmab\ICmabClient.cs"/>
208+
<Compile Include="Cmab\ICmabService.cs"/>
208209
<Compile Include="Cmab\DefaultCmabClient.cs"/>
210+
<Compile Include="Cmab\DefaultCmabService.cs"/>
209211
<Compile Include="Cmab\CmabRetryConfig.cs"/>
210212
<Compile Include="Cmab\CmabModels.cs"/>
211213
<Compile Include="Cmab\CmabConstants.cs"/>

0 commit comments

Comments
 (0)