Following the Options.Contextual docs:
1. Define your context class that will be passed to IContextualOptions.GetAsync()
.
By default UserId
property is used for allocation calculations. You can override it globally by configuring ExcosOptions
or override it for a single feature by configuring the AllocationUnit
property.
[OptionsContext]
internal partial class WeatherForecastContext
{
public Guid UserId { get; set; }
public string? Country { get; set; }
}
2. Define your options class like you usually would.
You can add a property of type FeatureMetadata
to your options model to receive experiment metadata, such as the variant IDs.
internal class WeatherForecastOptions
{
public string TemperatureScale { get; set; } = "Celsius"; // Celsius or Fahrenheit
public int ForecastDays { get; set; }
public FeatureMetadata? Metadata { get; set; }
}
3. Bind the options to a configuration section.
services.ConfigureExcos<WeatherForecastOptions>("Forecast");
4. Configure Excos experiment via configuration or code (see tests project for examples).
services.ConfigureExcosFeatures("Features");
{
"Features": {
"Forecast": {
"Variants": {
"Celsius": {
"Allocation": "100%",
"Settings": {
"Forecast": {
"TemperatureScale": "Celsius"
}
}
},
"Fahrenheit": {
"Allocation": "100%",
"Filters": {
"Country": "US"
},
"Settings": {
"Forecast": {
"TemperatureScale": "Fahrenheit"
}
}
}
}
}
}
}
5. Inject IContextualOptions
into your service.
internal class WeatherForecast
{
public DateTime Date { get; set; }
public int Temperature { get; set; }
public string TemperatureScale { get; set; } = string.Empty;
}
internal class WeatherForecastService
{
private readonly IContextualOptions<WeatherForecastOptions, WeatherForecastContext> _contextualOptions;
private readonly Random _rng = new(0);
public WeatherForecastService(IContextualOptions<WeatherForecastOptions, WeatherForecastContext> contextualOptions)
{
_contextualOptions = contextualOptions;
}
public async Task<IEnumerable<WeatherForecast>> GetForecast(WeatherForecastContext context, CancellationToken cancellationToken)
{
WeatherForecastOptions options = await _contextualOptions.GetAsync(context, cancellationToken);
return Enumerable.Range(1, options.ForecastDays).Select(index => new WeatherForecast
{
Date = new DateTime(2000, 1, 1).AddDays(index),
Temperature = _rng.Next(-20, 55),
TemperatureScale = options.TemperatureScale,
});
}
}
You can also allow overriding the variant based on some context - example:
class TestUserOverride : IFeatureVariantOverride
{
public Task<VariantOverride?> TryOverrideAsync<TContext>(Feature experiment, TContext optionsContext, CancellationToken cancellationToken)
where TContext : IOptionsContext
{
var receiver = new Receiver();
optionsContext.PopulateReceiver(receiver);
if (experiment.Name == "MyExp" && receiver.UserId.IsTestUser())
{
return new VariantOverride
{
Id = "MyVariant",
OverrideProviderName = nameof(TestUserOverride),
};
}
return null;
}
private class Receiver : IOptionsContextReceiver
{
public Guid UserId;
public void Receive<T>(string key, T value)
{
if (key == nameof(UserId))
{
UserId = (Guid)value;
}
}
}
}
-
Experiments
Configure variants with disjoint allocation ranges (e.g.
[0;0.5)
and[0.5;1]
). -
Feature rollouts
Configure a single variant with percentage allocation (e.g.
10%
, later updated to25%
, etc). -
Feature gates
Configure a single variant with 100% allocation and focus on filters to deliver the feature to the right audience.
- Match string (config: a string with no
*
) - Match regex (config: a simple string with
*
placeholders or a full on expression starting with^
) - In range (config: a
[ or ( xx; yy ) or ]
expression wherexx
andyy
is either Guid, DateTimeOffset or Double)