Skip to main content

The 1001st Piece in your 1000 Piece Puzzle: .NET Default Interface Functions

I was recently working 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. The reason why I had to write it that way was: 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. Resulting in all classes that implement 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);
    }
}

Not much to see here, it's just like every other interface you've ever written. 

Interface with default implementation

With this update to the interface, I've added default code to the GetUserId function. This will do a couple things: Centralize repeated code (remember to keep it DRY), and this function does not have 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);
    }
}

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

Using default implementation

In order to call the default implementation, the object needs to be cast to the interface type. Otherwise the runtime will look for an overridden version of the function on the implementing object.   

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 little syntactic sugar in the class 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 for this new language feature, I thought it was going to 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 functions in services that stood alone, 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, maybe it should be a default function on the appropriate interface. 

Comments

Popular posts from this blog

Optimizely Gets More (Case-)Sensitive

As Windows developers, we don't always have to pay attention to capitalization when dealing with paths and URIs. With CMS12, Optimizely has started deploying to a Linux container for hosting sites. This means that deployed sites (and developers!) will have to pay attention to capitals in references. Inconsistent capitalization can cause 404 errors in unexpected places. Thankfully there's a few ways to handle that!  The worst part of this is that developers won't be able to find these issues until deployed to Azure, with Windows, being case in-sensitive and all. Once named, files and folders can be difficult to change in git. Below are some ways to help rename your files so that they'll play nicely in Linux. Rename via Git mv Command   One way to update capitalization is by the git mv command. In you git command shell:  git mv <source> <target> This command will rename a file or folder, however it still runs in the context of Windows. If you want to simply ch

Config-Per-Site in Multi-Tenant Environments

Recently, a task was given to me at work where we needed a multisite configuration. We all know that multi-environment is as easy as appsettings.<environment_name>.config. What about in multi-tenant environments? You can't have appsettings.site1.config and appsettings.site2.config in your site! Well, not without a little extra work... Allow me to introduce  AddKeyPerFile ; this handy little function that you set up in your Program.cs will enumerate the files in a directory and add them to your Configuration.  "Alright!" you must be thinking "Show me how this miracle function works..." Well, let's get to it! First, you'll want to identify the config sections that will be unique to each site and put them in their own folder. I do this by site name because it makes the most sense by our conventions, if something else works for your practice, these names aren't set in stone.  Now, in order to suck those values into your config, there's one simp