Optimizely Integration with Optimizely CMS a Generic Approach

Optimizely Integration with Optimizely CMS a Generic Approach

This blog post will demonstrate how you can connect to Optimizely Full-stack using the dot net standard SDK and a optimization manager which will reduce the overall complexity for the implementation. So without further due. Lets begin.

First, we will need to install the Optimizely SDK in your solution. Go to the NuGet package, search for Optimizely and install it.

We will then, create an interface to define the behavior of the optimization manager. It will have two functions, one to know if the experience is going to be displayed or not and the other one that will also get you the variables for the experience defined.

public interface IOptimizationManager
{
    bool ShowExperience([CanBeNull] IIdentity userData, string featureKey, string variableKey);

    dynamic GetExperience<T>(IIdentity userData, string featureKey) where T : class, IOptimizationModel;
}

Now, we will create the Optimization Manager class which implements the interface and have three private properties: the data file URL, SDK key and the Optimizely client which come from the appsettings.json file.

  "Optimizely": {
    "DataFileUrl": "https://cdn.optimizely.com/datafiles/KEY.json",
    "SdkKey": "KEY"
  }

The class will also have the constructor which will not only initialize the private variables, but also will initialize the Optimizely client

public class OptimizationManager : IOptimizationManager
{
    private readonly string _datafileUrl;
    private readonly string _sdkKey;
    private Optimizely Optimizely { get; set; }

    public OptimizationManager(IConfiguration configuration)
    {
        _datafileUrl = configuration["Optimizely:DataFileUrl"];
        _sdkKey = configuration["Optimizely:SdkKey"];
        InitializeOptimizely();
    }
}

The initialize method will create a project config manager builder and use the values from the appsettings.json file a notification center and use all those objects to initialize the client. A callback method was also implemented to catch any configuration update from the Optimizely side, but we are not doing anything there yet.

    private void InitializeOptimizely()
    {
        var notificationCenter = new NotificationCenter();

        // fetch any datafile changes, which result from configuration updates you make to traffic percentage sliders, flag variable values, etc.
        var configManager = new HttpProjectConfigManager.Builder()
            .WithUrl(_datafileUrl)
            .WithSdkKey(_sdkKey)
            .WithPollingInterval(TimeSpan.FromSeconds(5))
            .WithNotificationCenter(notificationCenter)
            .Build(false);

        Optimizely = new Optimizely(configManager, notificationCenter);

        // Register notification listener which triggers whenever config get updated
        // Notification listener listens for config updates and re-runs the calculation
        notificationCenter.AddNotification(NotificationCenter.NotificationType.OptimizelyConfigUpdate,
            GetConfigCallback());
    }

    public static NotificationCenter.OptimizelyConfigUpdateCallback GetConfigCallback() =>
        () =>
        {
            // Nothing here yet
        };

The show experience method will identify if the user is logged in, will set the user attributes for the logged in user and then try to get the feature by key and the default variable for that experience. In this case a Boolean. We use Wangkanai detection package for the browser detection service.

    public bool ShowExperience(IIdentity userData, string featureKey, string variableKey)
    {
        if (userData?.Name == null || !userData.Name.Contains("@"))
        {
            return false;
        }
        var detectionService = ServiceLocator.Current.GetInstance<IDetectionService>();

        var attributes = new UserAttributes
        {
            { "logged_in", true }, { "browser_version", detectionService.Browser.Name.ToString().ToLower() }
        };

        var userId = EnvironmentHelper.GetEnvironmentName() + "_" + userData.Name;
        var user = Optimizely.CreateUserContext(userId, attributes);

        var decision = user.Decide(featureKey);
        return decision.Enabled && decision.Variables.GetValue<bool>(variableKey);
    }

The method get experience will again check if the user is logged in, then set the user attributes and then try to get the experience using the feature key, but with a big difference, we try to get all the different variables defined in the experiment so we can used them wherever is needed.

    public dynamic GetExperience<T>(IIdentity userData, string featureKey) where T : class, IOptimizationModel
    {
        if (userData?.Name == null || !userData.Name.Contains("@"))
        {
            return null;
        }

        var detectionService = ServiceLocator.Current.GetInstance<IDetectionService>();
        var attributes = new UserAttributes
        {
            { "logged_in", true }, { "browser_version", detectionService.Browser.Name.ToString().ToLower() }
        };

        var userId = EnvironmentHelper.GetEnvironmentName() + "_" + userData.Name;
        var user = Optimizely.CreateUserContext(userId, attributes);

        var decision = user.Decide(featureKey);
        return decision.Enabled ? GetModel(decision, typeof(T)) : null;
    }

The method that gets the custom model is appropriate called get model, it receives the decision from Optimizely and the type that is going to return. It uses reflection to generalize the implementation.

    private static dynamic GetModel(OptimizelyDecision decision, Type type)
    {
        var properties = JsonExtensions.PropertyNames(type);

        var ctor = type.GetConstructor(Type.EmptyTypes);
        var instance = ctor.Invoke(null);
        foreach (var property in properties)
        {
            var variableName = JsonExtensions.GetJsonPropertyName(type, property.name);

            var method = decision.Variables.GetType().GetMethod("GetValue").MakeGenericMethod(new Type[] { property.type });
            var variableValue = method.Invoke(decision.Variables, new object[] { variableName });

            var methodInfo = type.GetMethod($"set_{property.name}");
            methodInfo.Invoke(instance, new object[] { variableValue });
        }

        return instance;
    }

Finally, we have an interface for all Optimization models that we will create and is described below

public interface IOptimizationModel
{
    // Nothing here yet
}

A model will implement the Optimization model interface and look like this

    public class CreateAccountEmailModel : IOptimizationModel
    {
        [JsonProperty("template_id")]
        public string TemplateId { get; set; }
    }

where the Json property attribute will help to identify how the variable is being called in the Optimizely side. Do not forget to add the interface and the class implementation to your dependency injection container.

context.Services.AddSingleton<IOptimizationManager, OptimizationManager>();

Finally, to execute the methods you just call instantiate the manager using dependency injection. If the method is get experience it will return the decision as a model, if is the show experience method, it will return a Boolean value

 var optimizationManager = ServiceLocator.Current.GetInstance<IOptimizationManager>();
            var decision = optimizationManager.GetExperience<CreateAccountEmailModel>(User.Identity,
                                "create_account_email");

And that is it. You can now return any experiment and its variables from Optimizely Full-stack using the Optimizely SDK and a generic method which uses reflection to avoid duplicated code and simplify the implementation. If you have any questions or suggestions please let me know in the comments. I hope this can help someone and as always keep learning !!!

Written by:

Jorge Cardenas

Developer with several years of experience who is passionate about technology and how to solve problems through it.

View All Posts

Leave a Reply