.NET
Eppo's open source .NET SDK can be used for feature flagging and experiment assignment and multi-armed contextual bandits:
Getting Started
Installation
In your .NET application, add the Eppo.Sdk Package from Nuget.
dotnet add package Eppo.Sdk
Usage
Begin by initializing a singleton instance of Eppo's client with an SDK key from the Eppo interface. Once initialized, the client can be used to make assignments anywhere in your app. Initialization should happen when your application starts up to generate a singleton client instance, once per application lifecycle:
Initialize Once
var eppoClientConfig = new EppoClientConfig("<SDK_KEY>", null);
var eppoClient = EppoClient.Init(eppoClientConfig);
After initialization, the SDK begins polling Eppo's API at regular intervals to retrieve the most recent experiment configurations such as variation values and traffic allocation. The SDK stores these configurations in memory so that assignments thereafter are effectively instant. For more information, see the architecture overview page.
If you are using the SDK for experiment assignments, make sure to pass in an assignment logging callback (see section below).
Assign Anywhere
The GetStringAssignment
method take the following parameters:
flagKey
(string): The key of the feature flagsubjectKey
(string): The key of the subject or user assigned to the experiment variationsubjectAttributes
(IDictionary): The subject's attributesdefaultValue
(string): The default variation to return if the flag is not successfully evaluated, or, as is more common, the flag is disabled
var assignedVariation = eppoClient.GetStringAssignment(
'fresh-user-onboarding',
user.id,
user.attributes,
'control'
);
Define an Assignment Logger
Eppo is architected so that raw user data never leaves your system. As part of that, instead of pushing subject-level exposure events to Eppo's servers, Eppo's SDKs integrate with your existing logging system. The SDK invokes the callback to capture assignment data whenever a variation is assigned. This is done with a logging callback method defined at SDK initialization.
var eppoClientConfig = new EppoClientConfig(
"<SDK_KEY>",
new AssignmentLogger());
The code below illustrates an example implementation of a logging callback to the console and other event platforms such as Segment. You could also use your own logging system, the only requirement is that the SDK receives a LogAssignment(AssignmentLogData assignmentLogData)
method. The AssignmentLogData
class implements ISerializable
so most systems should be able to easily log the value.
- Console
- Segment
using eppo_sdk.dto;
using eppo_sdk.logger;
internal class AssignmentLogger : IAssignmentLogger
{
public void LogAssignment(AssignmentLogData assignmentLogData)
{
Console.WriteLine(assignmentLogData);
}
}
using eppo_sdk.dto;
using eppo_sdk.logger;
class SegmentLogger : IAssignmentLogger
{
private readonly Analytics analytics;
public SegmentLogger(Analytics analytics)
{
this.analytics = analytics;
}
public void LogAssignment(AssignmentLogData assignmentLogData)
{
analytics.Track("Eppo Randomization Assignment", assignmentLogData);
}
}
More details about logging and examples (with Segment, Rudderstack, mParticle, and Snowplow) can be found in the event logging page.
Assignment Methods
Every Eppo flag has a return type that is set once on creation in the dashboard. Once a flag is created, assignments in code are made using the corresponding typed method:
GetBooleanAssignment(...)
GetNumericAssignment(...)
GetIntegerAssignment(...)
GetStringAssignment(...)
GetJSONAssignment(...)
Each method has the same signature (except for the type of defaultValue
) and returns the type in the method name. For booleans use getBooleanAssignment
, which has the following signature:
public bool GetBooleanAssignment(
string flagKey,
string subjectKey,
Dictionary<string, object> subjectAttributes,
bool defaultValue
);
Advanced Options
Polling Interval
For additional control in server deployments, the EppoClientConfig
class can be initialized with a custom interval to override the default of 30sec.
var config = new EppoClientConfig("YOUR-API-KEY", myAssignmentLogger)
{
PollingIntervalInMillis = 5000
};
Assignment Log Schema
The SDK will invoke the LogAssignment
method with an event
object that contains the following fields:
Field | Description | Example |
---|---|---|
Experiment (string) | An Eppo experiment key | "recommendation-algo-allocation-17" |
Subject (string) | An identifier of the subject or user assigned to the experiment variation | "ed6f85019080" |
Variation (string) | The experiment variation the subject was assigned to | "control" |
Timestamp (DateTime) | The time when the subject was assigned to the variation | 2021-06-22T17:35:12.000Z |
SubjectAttributes (Map<String, object>) | A free-form map of metadata about the subject. These attributes are only logged if passed to the SDK assignment method | Map.of("device","iOS") |
FeatureFlag (string) | An Eppo feature flag key | "recommendation-algo" |
Allocation (string) | An Eppo allocation key | "allocation-17" |
Usage with Contextual Multi-Armed Bandits
Eppo also supports contextual multi-armed bandits. You can read more about them in the high-level documentation. Bandit flag configuration--including setting up the flag key, status quo variation, bandit variation, and targeting rules--are configured within the Eppo application. However, available actions are supplied to the SDK in the code when querying the bandit.
To leverage bandits using the Node SDK, there are two additional steps over regular feature flags:
- Add a bandit action logger to the SDK client instance
- Query the bandit for an action
Defining a Bandit Logger
In order for the bandit to learn an optimized policy, we need to capture and log the bandit's actions. This requires defining a bandit logger in addition to an assignment logger.
class SegmentLogger : IAssignmentLogger
{
private readonly Analytics analytics;
public SegmentLogger(Analytics analytics)
{
this.analytics = analytics;
}
public void LogAssignment(AssignmentLogData assignmentLogData)
{
analytics.Track("Eppo Randomization Assignment", assignmentLogData);
}
public void LogBanditAction(BanditLogEvent banditLogEvent)
{
analytics.Track("Eppo Bandit Action", banditLogEvent);
}
}
The SDK will invoke the LogBanditAction()
method with a BanditLogEvent
object that contains the following fields:
Field (Type) | Description | Example |
---|---|---|
Timestamp (DateTime) | The time when the action is taken in UTC as an ISO string | "2024-03-22T14:26:55.000Z" |
FlagKey (string) | The key of the feature flag corresponding to the bandit | "bandit-test-allocation-4" |
BanditKey (string) | The key (unique identifier) of the bandit | "ad-bandit-1" |
SubjectKey (string) | An identifier of the subject or user assigned to the experiment variation | "ed6f85019080" |
SubjectNumericAttributes (IDictionary<string, double>) | Metadata about numeric attributes of the subject. Map of the name of attributes their provided values | {"age": 30} |
SubjectCategoricalAttributes (IDictionary<string, string>) | Metadata about non-numeric attributes of the subject. Map of the name of attributes their provided values | {"loyalty_tier": "gold"} |
Action (string) | The action assigned by the bandit | "promo-20%-off" |
ActionNumericAttributes (IDictionary<string, double>) | Metadata about numeric attributes of the assigned action. Map of the name of attributes their provided values | {"brandAffinity": 0.2} |
ActionCategoricalAttributes (IDictionary<string, string>) | Metadata about non-numeric attributes of the assigned action. Map of the name of attributes their provided values | {"previouslyPurchased": false} |
ActionProbability (number) | The weight between 0 and 1 the bandit valued the assigned action | 0.25 |
OptimalityGap (number) | The difference between the score of the selected action and the highest-scored action | 456 |
ModelVersion (string) | Unique identifier for the version (iteration) of the bandit parameters used to determine the action probability | "v123" |
MetaData IDictionary<string, string> | Any additional freeform meta data, such as the version of the SDK | { "sdkLibVersion": "3.5.1" } |
Querying for a Bandit Action
To query the bandit for an action, use the GetBanditAction()
method. The most specific implementation of GetBanditAction()
takes the following parameters:
flagKey
(string): The key of the feature flag corresponding to the banditsubjectKey
(string): The key of the subject or user assigned to the experiment variationsubjectAttributes
(IDictionary<string, object?>): The subject's attributesactions
(IDictionary<string, IDictionary<string, object?>> ): Map of actions (by name) to their categorical and numeric attributesdefaultValue
(string): The default variation to return if the flag is not successfully evaluated, or, as is more common, the flag is disabled
var subjectAttributes = new Dictionary<string, object?>()
{
["age"] = 30, // Gets interpreted as a Numeric Attribute
["country"] = "uk", // Categorical Attribute
["pricingTier"] = "1" // NOTE: Deliberately setting to string causes this to be treated as a Categorical Attribute
};
var actions = new Dictionary<string, IDictionary<string, object?>>()
{
["nike"] = new Dictionary<string, object?>()
{
["brandLoyalty"] = 0.4,
["from"] = "usa"
},
["adidas"] = new Dictionary<string, object?>()
{
["brandLoyalty"] = 2,
["from"] = "germany"
}
};
var result = client.GetBanditAction(
"flag-with-shoe-bandit",
"user123",
subjectAttributes,
actions,
"default");
if (result.Action != null)
{
// Follow the Bandit action
RenderShoeAd(result.Action);
} else {
// User was not selected for a Bandit.
// A variation is still assigned.
RenderDefaultShoeAd(result.Variation);
}
GetBanditAction
Overloads / Alternative Parameters
There are a couple of additional overloads of the GetBanditAction()
method to call, depending on the shape of your input.
For a simple list of actions without attributes:
public BanditResult GetBanditAction(string flagKey,
string subjectKey,
IDictionary<String, object?> subjectAttributes,
string[] actions,
string defaultValue
For a simple list of actions without attributes, using a ContextAttributes
subject:
public BanditResult GetBanditAction(string flagKey,
ContextAttributes subject,
string[] actions,
string defaultValue)
Using ContextAttributes
objects for subject and actions:
public BanditResult GetBanditAction(string flagKey,
ContextAttributes subject,
IDictionary<string, ContextAttributes> actions,
string defaultValue)
Unsorted attributes for both subject and actions. The EppoClient
will automatically sort them into numeric (integer and float types), categorical (string, boolean) and emit a warning if other types are passed:
public BanditResult GetBanditAction(string flagKey,
string subjectKey,
IDictionary<string, object?> subjectAttributes,
IDictionary<string, IDictionary<string, object?>> actions,
string defaultValue)
ContextAttributes
The ContextAttributes
class bundles a context identifier (ex: SubjectKey
or ActionName
) along with the categorical and numeric attributes associated with that context. It can be built from a dictionary of unsorted attributes or from specified categorical/numeric attributes. It also functions like a Dictionary<string, object>
.
public static ContextAttributes FromDict(string key,
IDictionary<string, object?> other);
public static ContextAttributes FromNullableAttributes(string key,
IDictionary<string, string?>? categoricalAttributes,
IDictionary<string, object?>? numericAttributes);
// Use like an `IDictionary`
var myUserAttributes = new ContextAttributes("user123")
{
["age"] = 30,
["country"] = "uk",
["pricingTier"] = "1" // NOTE: Deliberately setting to string causes this to be treated as a categorical attribute
};
myUserAttributes["last30DaySpend"] = userService.GetRecentDaysSpend(30);
myUserAttributes.Add("profileCompletion", 0.50f);
Full Initialization and Assignment Example
class Program
{
public void main()
{
// Initialize Segment and Eppo clients.
var segmentConfig = new Configuration(
"<YOUR WRITE KEY>",
flushAt: 20,
flushInterval: 30);
var analytics = new Analytics(segmentConfig);
// Create a logger to send data back to the Segment data warehouse
var logger = new SegmentLogger(analytics);
// Initialize the Eppo Client
var eppoClientConfig = new EppoClientConfig("EPPO-SDK-KEY-FROM-DASHBOARD", logger);
var eppoClient = EppoClient.Init(eppoClientConfig);
// Elsewhere in your code, typically just after the user logs in.
var subjectTraits = new JsonObject()
{
["email"] = "janedoe@liamg.com",
["age"] = 35,
["accountAge"] = 2,
["tier"] = "gold"
}; // User properties will come from your database/login service etc.
var userID = "user-123";
// Identify the user in Segment Analytics.
analytics.Identify(userID, subjectTraits);
// Need to reformat user attributes a bit; EppoClient requires `IDictionary<string, object?>`
var subjectAttributes = subjectTraits.Keys.ToDictionary(key => key, key => (object)subjectTraits[key]);
// Get assignments
var showUpgradeAd = eppoClient.GetBooleanAssignment(
"show-upgrade-ad",
userID,
subjectAttributes,
false
);
// Get an assignment for the user
var recentUserTip = eppoClient.GetStringAssignment(
"recent-user-onboarding",
userID,
subjectAttributes,
"control"
);
// Get a bandit action
var actions = new List<String>(){"nike", "adidas"};
var banditResult = eppoClient.GetBanditAction(
"shoe-bandit",
userID,
subjectAttributes,
actions,
"default"
);
if (showUpgradeAd)
{
RenderUpgradeAd();
}
RenderRecentUserTip(recentUserTip);
if (result.Action != null)
{
RenderShoeAd(result.Action);
} else {
RenderDefaultShoeAd(result.Variation);
}
}
}
class SegmentLogger : IAssignmentLogger
{
private readonly Analytics analytics;
public SegmentLogger(Analytics analytics)
{
this.analytics = analytics;
}
public void LogAssignment(AssignmentLogData assignmentLogData)
{
analytics.Track("Eppo Randomization Assignment", assignmentLogData);
}
public void LogBanditAction(BanditLogEvent banditLogEvent)
{
analytics.Track("Eppo Bandit Action", banditLogEvent);
}
}