Dependency Injection

Use Dependency Injection to set CMS Function parameters automatically

Services added to the service collection during application startup can be injected into CMS Function parameters. To use this feature, simply declare a parameter of the type you want injected:

@inherits RazorFunction
@functions {
    public RoutedData<IMediaFile> MediaData { get; set; }
}

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
</head>
<body>
    @if (MediaData.IsList)
    {
        <h1>List view</h1>
        <ul>
            @foreach (IMediaFile media in MediaData.List.Take(25))
            {
                <li><a href="@MediaData.ItemUrl(media)">@media.Title</a></li>
            }
        </ul>
    }
    else
    {
        <h1>Item view: @MediaData.Item.Title</h1>
    }
</body>
</html>
In the above example (a Razor Function, where function parameters are defined as public properties), the object value for the MediaData parameter will be injected, provided the Service Locator can instantiate objects of the specified type (here RoutedData<IMediaFile>).

Why dependency injection?

You could obviously new up the object yourself, but using the above approach has a few advantages:

  1. Decoupling - the concrete creation (or even object sub type) is handled elsewhere and can be changed, without changing this code.
  2. Object life time - when you register a type with the service collection used by the Service Locator, you can control if objects should be constructed once and reused (singleton), be constructed once per user page request (scoped) or be constructed as new every time (transient). The consumer of the object need not care about this.
  3. More testable - because of the decoupling you can register mock objects with the service collection and execute the unchanged function to test.
  4. Ability to override - because the object value come through a parameter you can explicitly set the parameter if needed. C1 CMS will only inject parameters that have not been explicitly set.

The user experience?

When inserting CMS Functions, users can set values of their parameters via the GUI – in the “Function Properties” window. In this window, parameters that can be set via dependency injection, are explicitly omitted, meaning users will not see it in the simple view.

You can, however, switch to the Advanced view where all the parameters are shown and thus override these default values. Should you have another CMS Function that emit RoutedData<IMediaFile> you can hook this in, using the advanced view.

Using dependency injection with your own types

In terms of CMS Functions, an input parameter will serve as a “client” that will be “injected” with a “service” you are supposed to define and register in advance.

You can To set CMS Function parameters to default values using dependency injections:

  1. Define a service.
  2. Register the service.
  3. Use the registered service to define a parameter.

Defining a service

You can define your service in a custom DLL / class library (built and placed in /Bin).

To define a service:

  1. Define an interface or class.
  2. Declare a class that implements this interface or class.

In the following example, we'll define the interface IRandomNumberGenerator and implement it in the internal class RND.

    public interface IRandomNumberGenerator
    {
        int GetNext();
    }

    internal class RND : IRandomNumberGenerator
    {
        readonly Random _random = new Random();

        public int GetNext() => _random.Next(1000);
    }

Registering the service

To register your service, in your custom DLL make use of the [ApplicationStartup] attribute and add the optional parameter IServiceCollection services to the initialization handlers:

[ApplicationStartup]
public static class CmsStartup
{
    public static void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton(typeof (IRandomNumberGenerator), typeof (RND));
    }
}
C1 CMS is using Microsoft.Extensions.DependencyInjection library that defines the service collection API, for more information about service registration options, please check the documentation here.

The injected types can be used by declaring a parameter of a registered type on a CMS Function.

In the following example, we'll use the interface in a Razor function to output a random number:

@inherits RazorFunction
@using ClassLibrary1;     

@functions {
    public override string FunctionDescription
    {
        get  { return "Shows a random integer value"; }
    }
     
    [FunctionParameter]
    public IRandomNumberGenerator RandomNumberGenerator { get; set; }

}

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:f="http://www.composite.net/ns/function/1.0">
    <head>
    </head>
    <body>
        <h1>Random value of the moment is: @RandomNumberGenerator.GetNext() </h1>
    </body>
</html>

Accessing services from other places in C1 CMS

Registered services can be consumed by the other services via constructor injection, as well as consumed in the OnInitialized() method of a startup handler.

[ApplicationStartup]
public static class CmsStartup
{
    public static void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton(typeof (IRandomNumberGenerator), typeof (RND));
    }

    public static void OnBeforeInitialize()
    {
    }

    public static void OnInitialized(
        IRandomNumberGenerator randomNumberGenerator,
        IEnumerable<IDocumentProvider> documentProviders,
        IServiceProvider serviceProvider)
    {
        // randomNumberGenerator - getting a required service IRandomNumberGenerator
        // documentProviders - gets a collection of registered services of the given interface
        // serviceProvider - gives access to the service locator, allows resolving any registered interface
    }
}

As a last resort, the public static class Composite.Core.ServiceLocator exposes methods to get services.

To get the IRandomNumberGenerator service used above you can use the following:

var randomizer = ServiceLocator.GetService<IRandomNumberGenerator>(); 
Overriding default values

You can replace the value set with a dependency injection in the Advanced view of the Function Properties window by using a CMS Function that implements the interface and returns a different value.

In the following example, we'll use the C# function TestFunction to implement our interface IRandomNumberGenerator and return a hard-coded value.

public static IRandomNumberGenerator TestFunction()
{
    return new NotSoRandom();
}

internal class NotSoRandom : IRandomNumberGenerator
{
    public int GetNext() => 1234;
}

Requirements:

C1 CMS version 6.0 or later