Search code examples
c#asp.net.netsimple-injectorhangfire

WebApiRequestLifestyle and BackgroundJob Confusion


One of my dependencies (DbContext) is registered using the WebApiRequestLifestyle scope.

Now, my background job uses IoC and depends on the service that was registered above using the WebApiRequestLifestyle. I'm wondering how this works when Hangfire calls the method i registered for the background job. Will the DbContext be treated like a transistent object since the web api is not involved?

Any guidance would be great!

Here is my initialize code that occurs during start up:

    public void Configuration(IAppBuilder app)
    {
        var httpConfig = new HttpConfiguration();

        var container = SimpleInjectorWebApiInitializer.Initialize(httpConfig);

        var config = (IConfigurationProvider)httpConfig.DependencyResolver
            .GetService(typeof(IConfigurationProvider));

        ConfigureJwt(app, config);
        ConfigureWebApi(app, httpConfig, config);
        ConfigureHangfire(app, container);
    }
    private void ConfigureHangfire(IAppBuilder app, Container container)
    {
        Hangfire.GlobalConfiguration.Configuration
            .UseSqlServerStorage("Hangfire");

        Hangfire.GlobalConfiguration.Configuration
            .UseActivator(new SimpleInjectorJobActivator(container));

        app.UseHangfireDashboard();
        app.UseHangfireServer();
    }

public static Container Initialize(HttpConfiguration config)
{
    var container = new Container();
    container.Options.DefaultScopedLifestyle = new WebApiRequestLifestyle();

    InitializeContainer(container);

    container.RegisterMvcControllers(Assembly.GetExecutingAssembly());
    container.RegisterWebApiControllers(config);
    container.RegisterMvcIntegratedFilterProvider();

    container.Register<Mailer>(Lifestyle.Scoped);
    container.Register<PortalContext>(Lifestyle.Scoped);
    container.RegisterSingleton<TemplateProvider, TemplateProvider>();

    container.Verify();

    DependencyResolver.SetResolver(new SimpleInjectorDependencyResolver(container));

    config.DependencyResolver = new SimpleInjectorWebApiDependencyResolver(container);

    return container;
}

Here is my code that kicks off the background job:

public class MailNotificationHandler : IAsyncNotificationHandler<FeedbackCreated>
{
    private readonly Mailer mailer;

    public MailNotificationHandler(Mailer mailer)
    {
        this.mailer = mailer;
    }

    public Task Handle(FeedbackCreated notification)
    {
        BackgroundJob.Enqueue<Mailer>(x => x.SendFeedbackToSender(notification.FeedbackId));
        BackgroundJob.Enqueue<Mailer>(x => x.SendFeedbackToManagement(notification.FeedbackId));

        return Task.FromResult(0);
    }
}

Finally here is the code that runs on the background thread:

public class Mailer
{
    private readonly PortalContext dbContext;
    private readonly TemplateProvider templateProvider;

    public Mailer(PortalContext dbContext, TemplateProvider templateProvider)
    {
        this.dbContext = dbContext;
        this.templateProvider = templateProvider;
    }

    public void SendFeedbackToSender(int feedbackId)
    {
        Feedback feedback = dbContext.Feedbacks.Find(feedbackId);

        Send(TemplateType.FeedbackSender, new { Name = feedback.CreateUserId });
    }

    public void SendFeedbackToManagement(int feedbackId)
    {
        Feedback feedback = dbContext.Feedbacks.Find(feedbackId);

        Send(TemplateType.FeedbackManagement, new { Name = feedback.CreateUserId });
    }

    public void Send(TemplateType templateType, object model)
    {
        MailMessage msg = templateProvider.Get(templateType, model).ToMailMessage();

        using (var client = new SmtpClient())
        {
            client.Send(msg);
        }
    }
}

Solution

  • I'm wondering how this works when Hangfire calls the method i registered for the background job. Will the DbContext be treated like a transistent object since the web api is not involved?

    As the design decisions describe, Simple Injector will never allow you to resolve an instance outside an active scope. So that DbContext will neither be resolved as transient or singleton; Simple Injector will throw an exception when there's no scope.

    Every application type requires its own type of scoped lifestyle. Web API requires the AsyncScopedLifestyle (in previous versions WebApiRequestLifestyle), WCF an WcfOperationLifestyle and MVC the WebRequestLifestyle. For Windows Services you will typically use an AsyncScopedLifestyle.

    If your Hangfire jobs run in a Windows Service, you will have to use either a ThreadScopedLifestyle or the AsyncScopedLifestyle. Those scopes require explicit starting.

    When running the jobs on a background thread in a web (or Web API) application, there is no access to the required context and this means that Simple Injector will throw an exception if you try to do so.

    You however are using the Hangfire.SimpleInjector integration library. This library implements a custom JobActivator implementation called SimpleInjectorJobActivator and this implementation will create start a Scope for you on the background thread. Hangfire will actually resolve your Mailer within the context of this execution context scope. So the Mailer constructor argument in your MailNotificationHandler is actually never used; Hangfire will resolve this type for you.

    The WebApiRequestLifestyle and AsyncScopedLifestyle are interchangeable; the WebApiRequestLifestyle uses an execution context scope in the background and the SimpleInjectorWebApiDependencyResolver actually starts an execution context scope. So the funny thing is that your WebApiRequestLifestyle can be used for background operations as well (although it can be a bit confusing). So your solution works and works correctly.

    When running in MVC, however, this will not work, and in that case you would have to create a Hybrid lifestyle, for instance:

    var container = new Container();
    
    container.Options.DefaultScopedLifestyle = Lifestyle.CreateHybrid(
        new AsyncScopedLifestyle(),
        new WebRequestLifestyle());
    

    You can register your DbContext as follows:

    container.Register<DbContext>(() => new DbContext(...), Lifestyle.Scoped);
    

    Here's some feedback on your application's design, if you don't mind.

    Prevent letting application code, such as your MailNotificationHandler, from taking a direct dependency on an external library such as Hangfire. This is a direct violation of the Dependency Inversion Principle and makes your application code very hard to test and maintain. Instead, let solely your Composition Root (the place where you wire your dependencies) take a dependency on Hangfire. In your case, the solution is really straightforward and I would even say pleasant, and it would look as follows:

    public interface IMailer
    {
        void SendFeedbackToSender(int feedbackId);
        void SendFeedbackToManagement(int feedbackId);
    }
    
    public class MailNotificationHandler : IAsyncNotificationHandler<FeedbackCreated>
    {
        private readonly IMailer mailer;
    
        public MailNotificationHandler(IMailer mailer)
        {
            this.mailer = mailer;
        }
    
        public Task Handle(FeedbackCreated notification)
        {
            this.mailer.SendFeedbackToSender(notification.FeedbackId));
            this.mailer.SendFeedbackToManagement(notification.FeedbackId));
    
            return Task.FromResult(0);
        }
    }
    

    Here we added a new IMailer abstraction and made the MailNotificationHandler dependent on this new abstraction; unaware of the existence of any background processing. Now close to the part where you configure your services, define an IMailer proxy that forwards the calls to Hangfire:

    // Part of your composition root
    private sealed class HangfireBackgroundMailer : IMailer
    {
        public void SendFeedbackToSender(int feedbackId) {
            BackgroundJob.Enqueue<Mailer>(m => m.SendFeedbackToSender(feedbackId));
        }
    
        public void SendFeedbackToManagement(int feedbackId) {
            BackgroundJob.Enqueue<Mailer>(m => m.SendFeedbackToManagement(feedbackId));
        }
    }
    

    This requires the following registrations:

    container.Register<IMailer, HangfireBackgroundMailer>(Lifestyle.Singleton);
    container.Register<Mailer>(Lifestyle.Transient);
    

    Here we map the new HangfireBackgroundMailer to the IMailer abstraction. This ensures that the BackgroundMailer is injected into your MailNotificationHandler, while the Mailer class is resolved by Hangfire when the background thread is started. The registration of the Mailer is optional, but advisable, since it has become a root object, and since it has dependencies, we want Simple Injector to be aware of this type to allow it to verify and diagnose this registration.

    I hope you agree that from perspective of the MailNotificationHandler, the application is much cleaner now.