Skip to main content

Umbraco Package Migration to .NET Core: Criteria Providers - Extension Methods

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

Migrating Extension Methods

The functionality of the Personalisation Groups package is exposed via some extension methods added to Umbraco's IPublishedElement and UmbracoHelper. For example, given a set of content elements drawn from child nodes, a multi-node tree picker or a nested content property, you can show just the ones relevant to the user using something like this:

@foreach (var post in Model.Content.Children
    .Where(x => x.ShowToVisitor()))

Umbraco's rendering APIs haven't changed a great deal, so not much modification was needed for the immediate implementation of the extension methods, but when I started hooking into the package functionality itself a few questions were raised and decisions needed to be taken. These stemmed from, as discussed earlier, the stronger .NET Core emphasis on using dependency injection: providing what you need for a particular class via dependencies provided via the constructor. I've been adapting the code to better fit that paradigm, moving logic that I'd previously had in static methods into concrete classes, abstracted by an interface.

I'm fully signed up to that approach - whether it's leading to the pit of success for this package migration time will tell, but it certainly leads to a pit of more testable code, as I've already found by being able to put more functionality under unit tests that was tricky to do so before.

This does present a difficulty for extension methods though, which are static methods and hence can't hook up to the constructor injected dependency injection that the service classes I've created now use. It was a similar story with configuration. Previously I'd created and used a static wrapper class around the configuration values, giving me strongly typed access to them, but now these are injected via constructs like IOptions<PersonsalistionGroupConfig>, again they aren't easily accessible via static methods such as extension methods There are ways... but they feel a bit like going against the .NET Core paradigm shift - or at least, stronger emphasis - toward dependency injection..

To take a first step to resolve this situation, the first thing I've done is make the extension methods thinner, and delegate more functionality to the services layer. This resolves the problem with configuration, as now that's only needed in the service classes, where it can be injected in (see for example, here).

That stil leaves the issue though of how the methods available on this service are made available to the extension method. Newing it up is an option, but really defeats the purpose of using dependency injection. We could also consider service-location. But the most straightforward answers seems to be just to "pass it in" as a parmater to the method. By doing this, the extension method signature for the example shown above goes from this (for V8):

public static bool ShowToVisitor(this IPublishedElement content)

To this (V9):

public static bool ShowToVisitor(this IPublishedElement content,
    IGroupMatchingService groupMatchingService)

Which means in the template, we need to using the feature of .NET Core allowing to inject services into views, and then pass it along from there:

@inject IGroupMatchingService GroupMatchingService;

@foreach (var post in Model.Content.Children
    .Where(x => x.ShowToVisitor(GroupMatchingService)))

So is this "friendlier"?

This trade-off between best practice and testable code versus some additional complexity in the rendering layer is something Umbraco also have to consider, as they lean quite heavily on similar extension methods for the querying and rendering of content in templates. In order to try to provide the best of both worlds, they provide two versions of most of these methods. The first follows the practice of asking you to provide all the dependencies as parameters to the extension method, as I've shown above. The second uses a service locator pattern to retrieve the dependency, such that it doesn't need to be provided in the extension method siguature.

The former should certainly be used in all code where you can inject the necessary dependencies via the constructor - and I saw this myself as part of this migration work. I had a call to retrieve the value of a property using the extension method Value(this IPublishedContent, string alias). Whilst this should work at runtime, it was failing in my unit test with an error of: System.TypeInitializationException : The type initializer for 'Umbraco.Extensions.FriendlyPublishedContentExtensions' threw an exception. The reason being that the service location couldn't run within the context of a test, as all the necessary setup wasn't in place. Switching to the "unfriendly" (need a better term...) verison - Value(this IPublishedContent, IPublishedValueFallback publishedValueFallback, string alias) - I could gain access to the additional dependency via the constructor of the service, and provide a stub implementation for my test.

The latter though is what we'd likely use in templates, and it's debateable whether I should provide similar "friendly" extension methods for this package, that uses service locaton to resolve the IGroupMatchingService and delegates to method taking all parameters via it's signature. There's a few hoops to do this though - and as Umbraco's service locator is internal, it would mean fully opting in to this approach and implementing it myself in the package. So have decided to leave this for now and accept the additional "boilerplate" in the rendering.

To keep this to a minimum I've limited the extra dependency to a single service, via a bit of refactoring to create a single exposed service that itself can take more injected dependencies as is required through the constructor.

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.