Strategy Pattern and (One Reason) Why I Love Nancy

The open-closed principle is the second letter of the SOLID acronym. SOLID is a set of software design guidelines which help build better software. The open-closed principle declares that

software entities should be open for extension, but closed for modification

One way to satisfy this principle is through the use of the Strategy design pattern, sometimes called Policy. I’d like to show how easy it is to employ this pattern in reusable and extensible components for a Nancy application.

screwdrivers

While implementing my Argolis library I wanted to provide multiple extension points so that the behaviour can be changed by consumers. For example there is an interface called ISupportedClassMetaProvider which return some basic metadata about the given Type.

1
2
3
4
5
6
7
namespace Hydra.Discovery.SupportedClasses
{
    public interface ISupportedClassMetaProvider
    {
        SupportedClassMeta GetMeta(Type type);
    }
}

The default implementation DefaultSupportedClassMetaProvider looks at the type name and [Description] attribute to provide default label and description for a documented type. This can be changed by library consumers simply by creating a custom implementation from scratch, inheriting from the default implementation or decorating it.

Registering dependencies with Nancy

This is where Nancy comes in with it’s magnificent extensibility model. In Nancy there is IRegistrations interface and it’s abstract implementation called… Registrations. They provide simple abstraction over Dependency Injection, which is an integral part of Nancy. This abstraction will only let you do basic DI registrations, but it should be enough. Here’s an example setup which registers a shared instance of IAppSettings and an implementation of some IUserContext, which will be resolved per-request.

1
2
3
4
5
6
7
8
public class Registrations : Nancy.Bootstrapper.Registrations
{
    public Registrations()
    {
        Register<IAppSettings>(new WebConfigSettings());
        Register<IUserContext>(typeof(UserContext), Lifetime.PerRequest);
    }
}

Nancy will discover implementations of IRegistrations at startup and translate Register<>() calls to the underlying container. It is very important because Nancy supports (m)any dependency injection container(s) and otherwise it would not be possible to create reusable assemblies agnostic of the chosen DI library.

Injecting into Registrations

Because the very same container is used to create instances of IRegistrations, it is even possible to inject into them.

1
2
3
4
5
6
7
8
9
10
public class Registrations : Nancy.Bootstrapper.Registrations
{
    public Registrations(IAppSettings settings)
    {
        if (settings.IsDebug)
            Register<IUserContext>(typeof(TestUserContext), Lifetime.Singleton);
        else
            Register<IUserContext>(typeof(UserContext), Lifetime.PerRequest);
    }
}

Depending on the container used however this comes with a limitation that the injected type must either:

  • be a concrete type (resolving concrete types automatically is supported eg. by Unity or the bundled TinyIoC)
  • be registered up-front in the Bootstrapper

Providing defaults registrations to consumers

Because Nancy provides a container-agnostic way for registering dependencies and will discover them at startup it is trivially easy to bundle defaults within a reusable package. In addition to the above it is also possible to register defaults, which will only be used if no consumer-defined type is found. :tada:

In Argolis I have a HydraRegistrations class, which defines defaults. It is also possible to register multiple implementations of which can be resolved as IEnumerable<>.

Here’s an excerpt showing default registration for the ISupportedClassMetaProvider and three defaults for IPropertyRangeMappingPolicy.

1
2
3
4
5
6
7
8
9
10
11
12
13
public class HydraRegistrations : Nancy.Bootstrapper.Registrations
{
    public Registrations()
    {
        RegisterWithDefault<ISupportedClassMetaProvider>(typeof(DefaultSupportedClassMetaProvider));
        RegisterWithUserThenDefault<IPropertyRangeMappingPolicy>(new[]
        {
            typeof(XsdDatatypesMappingPolicy),
            typeof(XsdDatatypesNullablesMappingPolicy),
            typeof(SupportedClassRangeMappingPolicy),
        });
    }
}

RegisterWithDefault<>() means that if not other implementation is found within user code, the type DefaultSupportedClassMetaProvider will be used.

RegisterWithUserThenDefault<>() means that the three given types will be used alongside any consumer’s implementations.

Switching the policy

So how does the user provide a different implementation of ISupportedClassMetaProvider? The easy way is simply creating an implementation and it will be registered instead. Alternatively it is possible to create another registrations class and register there.

1
2
3
4
5
6
7
public class ConsumerRegistrations : Nancy.Bootstrapper.Registrations
{
    public Registrations()
    {
        Register<ISupportedClassMetaProvider>(typeof(MyBetterProvider));
    }
}

Super-duper-happy-path at its best

I can’t state enough how much I :sparkling_heart: this. Thanks to the Registrations class it is possible to create reusable Nancy libraries, which don’t require any unnecessary setup.

No boilerplate, no NuGet (:poop:) code generated from text templates, no DI-container dependency. It is just works out of the box. :sparkles:

Show me how your .NET web framework does it… :wink:

Comments