Dependency Injection for CMSApp and ITask, manual execution missing HttpContext.Current

Dean Lynn asked on February 17, 2022 10:40

I am currently working on a Kentico Xperience 13 build; this question focusses on the CMS aspect of our solution.

I am following this helpful article regarding Dependency Injection, written by Sean Wright:

https://dev.to/seangwright/kentico-12-design-patterns-part-4-adding-dependency-injection-to-the-cms-35a

My question is regarding the implementation of DI on ITask (scheduled tasks).

I am able to configure Autofac using property injection as per the above article, this works well when running the task via the scheduler.

However, when manually triggering the task (via the Scheduled tasks app UI) the constructor for my ITask implementation is executed twice.

The first execution includes a reference to HttpContext.Current which allows access to the container. The task's Execute() method is not triggered on this first run.

The second execution does not include a reference to HttpContext.Current therefore the container cannot be retrieved. The task's Execute() method is triggered on this second run but fails due to the missing dependencies.

I have a few queries I was hoping to answer regarding the above:

Why does Kentico create two instances of the ITask when triggering manually, compared to the one instance when triggered via the scheduler?

Is there additional configuration I may have missed (not covered in the article above) which could result in this behaviour?

Are there any alternative implementations of DI on ITask which work both via the scheduler and manual execution?

Thanks!


Update: 17 February 2022 11:16

It appears that selecting to run the task in a separate thread via the task properties in Kentico has the same effect on its execution.

Recent Answers


Dean Lynn answered on February 17, 2022 16:00 (last edited on February 17, 2022 16:11)

Looks like this problem is related to the execution of the task outside of an HTTP request context and the code's reliance on the existence of HttpContext.Current to retrieve the Autofac container.

It would appear that different methods of executing the tasks (manual or scheduled and both options with/without separate threads) result in differences in the availability of HttpContext.Current.

I believe access to the container for all scenarios can be fixed through an additional accessor method in the Global.asax.cs file, exposing the container directly rather than through HttpContext.Current.

Autofac's documentation uses a static container which it exposes via a public property:

public class Global : HttpApplication, IContainerProviderAccessor
{
  // Provider that holds the application container.
  static IContainerProvider _containerProvider;

  // Instance property that will be used by Autofac HttpModules
  // to resolve and inject dependencies.
  public IContainerProvider ContainerProvider
  {
    get { return _containerProvider; }
  }
  ...
}

https://autofac.readthedocs.io/en/latest/integration/webforms.html#implement-icontainerprovideraccessor-in-global-asax

I would assume that a public static method returning the same static container may allow access to that container outside of HttpContext.Current.

This appears to work, my task runs in a thread and can resolve the services I currently rely on.

I assume that this approach may raise other issues related to the absence of the context and potentially also with the scoping of services.

I would be welcome to hear any comments on the above.

Thanks.

0 votesVote for this answer Mark as a Correct answer

Sean Wright answered on April 4, 2022 21:06

Dean,

This is one of the major issues with DI in the CMS WebForms applications. A traditional WebForms app could use Autofac's integration with mild success (though WebForms was never designed for DI so it's always going to be an awkward fit).

Kentico Xperience has an internal DI container that can be interacted with in both the ASPNET Core app (though normal DI) and through the CMS app (using service location).

The service location syntax looks like this:

var service = Service.Resolve<IActivityLogService>();

This container does not support scoped services (which means it doesn't need an HttpContext to function correctly).

You can read more about interacting with the internal DI system here:

One of the MVPs wrote about this topic:

As did I:

0 votesVote for this answer Mark as a Correct answer

Sander Geerts answered on July 21, 2022 16:04 (last edited on July 21, 2022 16:05)

I have read all those articles but still struggle to find the 'correct' way to implement Dependency Injection into the CMS. This is because we have one solution (and shared projects) which expose services and the necessary di registrations for both CMS & MVC like so:

// file: MyProject.Business.ServiceCollectionExtensions
namespace Microsoft.Extensions.DependencyInjection
{
    public static class ServiceCollectionExtensions
    {
        public static IServiceCollection AddMyBusiness(this IServiceCollection services)
        {
            services.AddScoped<IMyBusiness, MyBusiness>();
        }
    }
}

This way we can do in the CMS as well in our MVC hosting site the following:

services.AddMyBusiness();

So we 'must' support scoped services too to. In our application start of the CMS we then do this:

MyBusiness.Common.ServiceLocator.ResolverFunction = (type) =>
{
    if (HttpContext.Current == null)
    {
        using (var scope = _serviceProvider.CreateScope())
        {
            return scope.ServiceProvider.GetService(type);
        }
    }
    else
    {
        return _serviceProvider.GetService(type);
    }
}

This creates a new scope for every requested service, so results practically in transient resolutions.

The other option would be to create a scope everytime I want to request services, something like this:

public class MyScheduledTask : ITask
{
    public string Execute(TaskInfo task)
    {
        var serviceProvider = MyBusiness.Common.ServiceLocator.Resolve<IServiceProvider>();
        using (var scope = serviceProvider.CreateScope())
        {
            var myService = scope.ServiceProvider.GetService<IMyService>();
            var myOtherService = scope.ServiceProvider.GetService<IMyOtherService>();
        }
    }
}
0 votesVote for this answer Mark as a Correct answer

   Please, sign in to be able to submit a new answer.