Skip to main content

Umbraco Package Migration to .NET Core: Criteria Providers - Wiring It All Up

This is one of a series of posts looking at migrating Umbraco packages to V9 and .NET Core. Other posts in this series:

  1. Introduction
  2. A False Start
  3. A Clean Start - Controllers, Services, Configuration and Caching
  4. Criteria Providers - Working With HttpContext
  5. Leaning on Umbraco
  6. Migrating Tests
  7. Extension Methods
  8. Migrations
  9. Wiring It All Up
  10. Distributing and Wrapping Up

Dependency Registration and other Start-up Tasks

At this point I've gone some package functionality that compiles and some unit tests that pass, but there's no way to actually run it yet and it won't work if somehow installed into an Umbraco V9 website. In order for that to happen I need to ensure that the services I've created, which are being constructor injected into various classes that need them, are registered with the IoC container. I've also got to make sure the package migration will run and some custom routes are in place for the controllers that the back-office UI will call (e.g. to get a list of available criteria to pick from when creating a personalisation group definition).

The Umbraco way of doing this since V8 is to use composers and components, and these are still available in V9. The more typical .NET Core way of integrating dependencies into an application though is by configuring the pipeline with code. For example, if you wanted to use a library like Automapper, you would install from NuGet, and then in the Startup.cs file use extension methods provided by the library to hook-up the functionality. So I've decided to follow that.

As well as just doing so to match .NET Core conventions, I'm free to do so - at least for now - as I'm considering only providing a NuGet install for the package rather than being able to install directly into the back-office. If that support is desired for other packages, then the composer method will still be required, as the idea is that the package will work without having to write any C# code. But given I've loosened this restruction for my case, I don't need to worry about that.

It's worth sharing a discussion I had with Bjarke (who heads up the team for the Umbraco .NET Core transition) who pointed out that you can do both. Have a component that's not much more than a one-liner, calling the extension methods you provide to be used in StartUp. That way, there's default configuration that the package will use if installed from the back-office, but, if a developer wants to, they can add code to call your extension method and potentially provide options to override certain behaviour. If doing this though you'd need to take care the registration logic doesn't run twice, or is made idempotent so there's no harm if it does.

Service Configuration

If you've ever done anything with ASP.NET Core web applications you'll be familiar with a couple of methods in start-up that can be used to wire up your applications - whether it's the adding of framework functionaity such as MVC or applying something specific for your application. The first of these is ConfigureServices(IServiceCollection services). To avoid what would otherwise likely become quite a big method, there's a pattern used of providing extension methods to adapt the service collection provided as a parameter to the method. In order to provide options for different installations, Umbraco has a slight variation on this, via a builder pattern, in that they have one method that extends the service collection called AddUmbraco() which returns an IUmbracoBuilder, and then further extensions on that to AddBackOffice() and AddWebsite() - that you can see in the default templates for a new Umbraco V9 application.

Hence I've followed this, creating an extension method AddPersonalisationGroups(), providing the already resolved application configuration as a parameter, and including it after Umbraco's AddComposers() builder extension:

public void ConfigureServices(IServiceCollection services) =>
    services.AddUmbraco(_env, _config)
        .AddBackOffice()
        .AddWebsite()
        .AddComposers()
        .AddPersonalisationGroups(_config)
        .Build();
Registering Configuration

The first thing the extension method does is register the package configuration as a strongly-typed object with the IoC container. This is using standard .NET Core methods, where we retrieve the named configuration section and provide a typed representation of it - in my case, PersonalisationGroupsConfig, viewable here. You'll see that for each configuration value I've made sure there's a default, which makes configuration of the package optional.

Should you wish to configure it for non-default behaviour though, Umbraco's configuration by default ships in an appSettings.json file, organised like this:

{
  "ConnectionStrings": {
    "umbracoDbDSN": ""
  },
  "Serilog": {
    ...
  },
  "Umbraco": {
    "CMS": {
      ...
    }
  }
}

The plan is that package configuration will also sit under the Umbraco element, so at some point an application using various packages, including this one, might look like this:

{
  "ConnectionStrings": {
    "umbracoDbDSN": ""
  },
  "Serilog": {
    ...
  },
  "Umbraco": {
    "CMS": {
      ...
    },
    "Forms": {
      ...
    },
    "PersonalisationGroups": {
      ...
    }    
  }
}

With the configuration registered, it's now available to be resolved via the constructor of controller and other classes via IOptions<PersonalisationGroups>

One last point on configuration, which I've not had a need for but there are examples in core, is that it's possible to create validation classes for your configuration. By doing this we can ensure that the configuration provided via the JSON file is valid, and if it's not, our application wil fail early at boot time rather than later, and more obsurely, when the configuration is used. The core examples are found under Umbraco.Core/Configuration/Models/Validation and are wired up with e.g.:

builder.Services.AddSingleton<IValidateOptions<GlobalSettings>, GlobalSettingsValidator>();
Registering Services

After configuration, services are registered using Umbraco's light wrapper of the MSDI abstractions. For example I call services.AddUnique<ICriteriaService, CriteriaService>()>; to ensure only a single instances of my interface ICriteriaService is registered with the required implementation of CriteriaService. Any class requesting that interface as a dependency will receive a copy of the registered concrete class.

Note that by providing the configuration as a parameter to the extension method, I can use that to customise which services are registered:

switch (configSection.GetValue<CountryCodeProvider>("CountryCodeProvider"))
{
    case CountryCodeProvider.MaxMindDatabase:
        services.AddUnique<ICountryCodeProvider, MaxMindCountryCodeFromIpProvider>();
        break;
    case CountryCodeProvider.CdnHeader:
        services.AddUnique<ICountryCodeProvider, CdnHeaderCountryCodeProvider>();
        break;
}

Application Configuration

Going back to StartUp, the second method we need to augment is Configure(IApplicationBuilder app), which is where, now all services are registered, application start-up tasks can be triggered. Again, following the conventions of .NET Core and Umbraco - who for example have app.UseUmbracoBackOffice() and app.UseUmbracoWebsite() - I've created an extension method UsePersonalisationGroups(), viewable here.

The first of two tasks I have is to setup routing for controllers, which is possible using similar similar code to what we used in .NET Framework, documented here. I've got a "to do" here to figure out how to add, or if I still need namespace constraints, in case another package happened to use the same controller names, but otherwise this worked as expected.

The second task is to run the migration, which - following a bit of service location to get some Umbraco dependencies like IScopeProvider, necessary as we're in a static extenion method - we can execute here (see ExecuteMigrationPlan() in the class linked above).

Of course it didn't all work first time, but after I'd fixed the obvious errors I was left with one issue which was that it didn't seem possible to create, save and publish a content item at this time. That may be an alpha release bug that will need looking into; for now I just contented myself with saving and not publishing the content item, and leaving that last step for the editor.

Following Along

The repository containing the code for the migrated package is here. At the time of writing, the state of the migrated code can be seen using this link.

Comments