I recently worked with a client who wanted a reasonably large subsystem added to Optimizely that would add automated management to their content. While cutting the code for this, I found myself writing similar code across multiple classes. I had to write it that way: 1) The client was currently on CMS11 and didn’t have access to newer language features; 2) The hierarchy of the classes prevented me from inserting a common ancestor. Thankfully, .NET has expanded the functionality of interfaces, so we can take advantage of those within Optimizely.

With .NET 5, Microsoft introduced default implementations on interfaces. Now interfaces can bring along a default implementation. All classes implementing the interface can use the default implementation or override it with custom logic. Enough text! Let’s code!

Original Interface

The following code is something that we’d create for an Optimizely experiment:
using OptimizelySDK;
using OptimizelySDK.Entity;

namespace Teapot.Interfaces.Services
{
public interface IExperimentation
{

public OptimizelyUserContext CreateUserContext(UserAttributes userAttributes = null, EventTags eventTags = null);
public string GetUserId();

public void TrackEvent(string eventKey);
}
}

There’s not much to see here; it’s just like every other interface you’ve written.

Interface with .NET Default Implementation

With this update to the interface, I’ve added the default code to the GetUserId function. This will do a couple things: Centralize repeated code (remember to keep it DRY), and this function will not need to be implemented separately when a class utilizes the interface.
using OptimizelySDK;
using OptimizelySDK.Entity;
using Perficient.Infrastructure.Interfaces.Services;
using System;

namespace Teapot.Interfaces.Services
{
    public interface IExperimentation
    {
             
        public OptimizelyUserContext CreateUserContext(UserAttributes userAttributes = null, EventTags eventTags = null);
        public string GetUserId(ICookieService cookieService)
        {
            var userId = cookieService.Get(“opti-experiment-testA”);
            if (userId == null)
            {
                userId = Guid.NewGuid().ToString();
                cookieService.Set(“opti-experiment-testA”, userId);
            }

            return userId;
        }

        public void TrackEvent(string eventKey);
    }
}

The default implementation can be overridden as needed, as with all interface functions. Who hasn’t run into an exception to the rule in their code?

Using Default Implementation

The object must be cast to the interface type to call the default implementation. Otherwise, the runtime will look for an overridden version of the function on the implementing entity.
return _featureExpermentation.CreateUserContext((this as IExperimentation).GetUserId(_cookieService));

If the default implementation is called often, it makes for some ugly code. This can be remedied with a bit of syntactic sugar in the class by leveraging the interface:

public string GetUserId()
{
return (this as IExperimentation).GetUserId(_cookieService);
}

Thoughts

I wanted to post this earlier, but I spent some time considering where this paradigm shift sits in my current Opti architecture. “This doesn’t fit the traditional view of Domain-Driven Design!” was my first thought. As much as I was excited about this new language feature, I thought it would be the stork-beak pliers in my coding toolbox. As I tried a few things out and tinkered around with default functions, I thought back to all the times I wrote parts in services that stood alone, with no arguments or services required to do the job. These are the little places that I think a default interface implementation can fill in. Alleviating some of the bloat (we’ll never get all of it) from services.
My rule of thumb from here has been that if a method is acting on the object with no outside dependencies, it may be a default function on the appropriate interface.