fluentscheduler / FluentScheduler

Automated job scheduler with fluent interface for the .NET platform.
Other
2.68k stars 410 forks source link

Dependency injection help #271

Open tallesl opened 4 years ago

tallesl commented 4 years ago

There are some use cases of the library that we don't officially support, but we embrace any community effort on helping folks in need.

Using the library with dependency injection is one of those cases. If you have anything to say on this topic (bug report, question, suggestion, opinion, etc) please comment on this issue instead of opening a new one.

tallesl commented 4 years ago

It is still in my todo list to offer a proper example in the docs, in the meanwhile these couple of comments may help giving an overall idea [1] [2].

mrgfisher commented 4 years ago

I have a few (small) programs that are structured using DI, broadly the approach is:

  1. Register services during startup (as you would expect) i.e. calls like appServices.AddTransient(serviceProvider => new Lazy(serviceProvider.GetRequiredService));

  2. Save the 'service provider' or equivalent in the program startup internal class Program { public static ServiceProvider ServiceProvider; .... var appServices = new Microsoft.Extensions.DependencyInjection.ServiceCollection(); ServiceProvider = appServices.BuildServiceProvider();

  3. Allow FluentScheduler to do its thing

  4. Then inside the Execute method of the scheduled logic you can do something like: var command = Program.ServiceProvider.GetService(); command.Execute(); Naturally implementations of IProcessBuildHourlyStatsCommand have lots of constructor requirements, but these are satisfied 'magically' :-)

To be clear, I only register the services used by the scheduled jobs, not the jobs themselves.

Hope this helps (anyone), am happy to provide more detailed code snippets if helpful.

YZahringer commented 4 years ago

A simple solution, without specific DI framework dependency, would be to provide an IScheduleBuilder that can be registered as a singleton. Then this could be injected and used to create an ISchedule.

In ASP.NET Core, this allows the IScheduleBuilder to be injected into a IHostedService, which is responsible to create, maintain and starting/stoping the ISchedule.

Something like that:

public interface IScheduleBuilder 
{
    ISchedule Create(Action job, string cronExpression);
    ISchedule Create(Action job, Action<RunSpecifier> specifier));
}

public interface ISchedule
{
    bool Running { get; }
    DateTime? NextRun { get; }

    event EventHandler<JobStartedEventArgs> JobStarted;
    event EventHandler<JobEndedEventArgs> JobEnded;

    void UseUtc();
    void ResetScheduling();
    void SetScheduling(Action<RunSpecifier> specifier);
    void SetScheduling(string cron);
    void Start();
    void Stop();
    void StopAndBlock();
    void StopAndBlock(int timeout);
    void StopAndBlock(TimeSpan timeout);
}

public class MyHostedService : IHostingService
{
    private readonly IServiceScopeFactory _serviceScopeFactory;
    private readonly ISchedule _myScheduleTask;

    public MyHostedService(IServiceScopeFactory scopeFactory, IScheduleBuilder scheduleBuilder)
    {
        _myScheduleTask = scheduleBuilder.Create(MyTask, "* * * * *");
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _myScheduleTask.Start();
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        _myScheduleTask.Stop();
    }

    private void MyTask()
    {
        using var scope = _serviceScopeFactory.CreateScope();
        var myJob = scope.ServiceProvider.GetRequiredService<IMyJob>();
        myJob.Execute();
    }
}

Another solution, coupled with ASP.NET Core DI and more plug-and-play, would be to support an AddSchedule<TJob>(Action<ScheduleOptions> options) where TJob : IScheduleJob extension method to IServiceCollection. An IHostedService would be registered automatically, it would create and configure the different ISchedule in background.

danilobbezerra commented 4 years ago

I have a few (small) programs that are structured using DI, broadly the approach is:

  1. Register services during startup (as you would expect) i.e. calls like appServices.AddTransient(serviceProvider => new Lazy(serviceProvider.GetRequiredService));
  2. Save the 'service provider' or equivalent in the program startup internal class Program { public static ServiceProvider ServiceProvider; .... var appServices = new Microsoft.Extensions.DependencyInjection.ServiceCollection(); ServiceProvider = appServices.BuildServiceProvider();
  3. Allow FluentScheduler to do its thing
  4. Then inside the Execute method of the scheduled logic you can do something like: var command = Program.ServiceProvider.GetService(); command.Execute(); Naturally implementations of IProcessBuildHourlyStatsCommand have lots of constructor requirements, but these are satisfied 'magically' :-)

To be clear, I only register the services used by the scheduled jobs, not the jobs themselves.

Hope this helps (anyone), am happy to provide more detailed code snippets if helpful.

Pls share your code with us

dev-greene commented 3 years ago

I'm using IMediatr and FluentScheduler, and have developed this approach.

It's setup in Startup.cs simply: JobManager.Initialize(new MyTaskRegistry (app));

And then uses a dispatch wrappper to properly queue mediatr commands. In this MyMediatrCommand and MyLocalMediatrCommand are mediatr commands I've defined elsewhere

/// <summary>
/// Fluent Scheduler Task Registry that runs jobs on an automated schedule.
/// https://github.com/fluentscheduler/FluentScheduler
/// </summary>
public class MyTaskRegistry : Registry
{
    public MyTaskRegistry(IApplicationBuilder app)
    {
        // Get Dependencies
        var dispatcher = app.ApplicationServices.GetService<IMediator>();
        var environment = app.ApplicationServices.GetService<IWebHostEnvironment>();

        // Set so a second instance of a job won't be fired until the first has completed
        NonReentrantAsDefault();

        // Jobs for all non-local environments
        if (!environment.IsEnvironment("Local"))
        {
            Schedule(new DispatchJob<MyMediatrCommand, int>(dispatcher))
                .ToRunEvery(5).Minutes();
        }

        // Local Jobs for testing
        if (environment.IsEnvironment("Local"))
        {
            Schedule(new DispatchJob<MyLocalMediatrCommand, bool>(dispatcher, new MyLocalMediatrCommand()))
                .ToRunEvery(5).Seconds();
        }
    }

    /// <summary>
    /// Job wrapper that uses Mediator to dispatch a command. <br/>
    /// Ensures NonReEntrant policies are followed.
    /// </summary>
    private class DispatchJob<TRequest, TResponse> : IAsyncJob where TRequest : IRequest<TResponse>, new()
    {
        private readonly IMediator _dispatcher;
        private readonly IRequest<TResponse> _request;

        public DispatchJob(IMediator dispatcher, TRequest request)
        {
            _dispatcher = dispatcher;
            _request = request;
        }

        public DispatchJob(IMediator dispatcher)
        {
            _dispatcher = dispatcher;
            _request = new TRequest();
        }

        public async Task ExecuteAsync()
        {
            await _dispatcher.Send(_request);
        }

    }
}
wapco commented 1 year ago

public async Task StartAsync(CancellationToken cancellationToken) { JobManager.Initialize(new SchedulerRegistry(_serviceProvider)); }

public class SchedulerRegistry : Registry { public SchedulerRegistry(IServiceProvider serviceProvider) { Schedule(() => { using var scope = serviceProvider.CreateScope(); return scope.ServiceProvider.GetRequiredService(); }).ToRunNow().AndEvery(ClientSetting.HeartbeatInterval).Seconds(); } }

phoenixcoded20 commented 1 year ago

NonReentrantAsDefault();

@dev-greene Why set this, why not run parallel when it's a DI?

GusBeare commented 1 year ago

I am not sure if this is ideal/correct or not but I use IServiceScopeFactory. I've an app running .Net 7 currently but that began with .Net 5. It's been stable through all versions and works very well. I realised that I need to pass several services into my jobs such as DB repo and logging and this is the way I found to do it.

In Program.cs.

using FluentScheduler;
using Microsoft.Extensions.DependencyInjection;  // IServiceScopeFactory

// Fluent scheduler
IServiceScopeFactory serviceScopeFactory = app.Services.GetRequiredService<IServiceScopeFactory>();
JobManager.Initialize(new ScheduleRegistry(serviceScopeFactory));

Then Registry.cs.

using FluentScheduler;
using Microsoft.Extensions.DependencyInjection;

namespace MyCode.Code.Fluent_Scheduler;

public class ScheduleRegistry : Registry
{
    public ScheduleRegistry(IServiceScopeFactory serviceScopeFactory)
    {
        // check the bookings every minute for unpaid after 10 mins and set to abandoned
        Schedule(() => new BookingCleanupJob(serviceScopeFactory)).NonReentrant().ToRunNow().AndEvery(1).Minutes();

        // run the proc to clear out zombie rows once a day
        Schedule(() => new ZombieCleanupJob(serviceScopeFactory)).NonReentrant().ToRunEvery(1).Days();
    }
}

Then my job (in this case BookingCleanup) looks like this:

using System;
using System.Collections.Generic;
using System.Linq;

namespace MyCode.Code.Fluent_Scheduler;

public class BookingCleanupJob : IJob
{
    private IServiceScopeFactory serviceScopeFactory;

    public BookingCleanupJob(IServiceScopeFactory serviceScopeFactory)
    {
        this.serviceScopeFactory = serviceScopeFactory;
     }

    // executes the scheduled task
    public async void Execute()
    {
        using var serviceScope = serviceScopeFactory.CreateScope();
        ISqlDatabase _repository = serviceScope.ServiceProvider.GetService<ISqlDatabase>();
        IConfiguration _config = serviceScope.ServiceProvider.GetService<IConfiguration>();
        IWebHostEnvironment _env = serviceScope.ServiceProvider.GetService<IWebHostEnvironment>();
        ILogging _logging = serviceScope.ServiceProvider.GetService<ILogging>();

       try
       {
            const string sql = "; EXEC [dbo].[sp_SetAbandonedBookingsJob]";
            var AbandonedBookings = await _repository.GetListAsync<Booking>(sql) as List<Booking>;

           // logging example
            var desc = "sp_SetAbandonedBookingsJob was called and updated: " + AbandonedBookings.Count + " rows.";
            await _logging.LogAsync(Constants.LogType_BookingCleanupJobRUN, null, null, desc);

This has worked really well for me and the application has remained stable for several years now. I've tested upgrade to preview .Net 8 and it seemed fine.

Rpgdudester commented 10 months ago

I am not sure if this is ideal/correct or not but I use IServiceScopeFactory. I've an app running .Net 7 currently but that began with .Net 5. It's been stable through all versions and works very well. I realised that I need to pass several services into my jobs such as DB repo and logging and this is the way I found to do it.

In Program.cs.

using FluentScheduler;
using Microsoft.Extensions.DependencyInjection;  // IServiceScopeFactory

// Fluent scheduler
IServiceScopeFactory serviceScopeFactory = app.Services.GetRequiredService<IServiceScopeFactory>();
JobManager.Initialize(new ScheduleRegistry(serviceScopeFactory));

Then Registry.cs.

using FluentScheduler;
using Microsoft.Extensions.DependencyInjection;

namespace MyCode.Code.Fluent_Scheduler;

public class ScheduleRegistry : Registry
{
    public ScheduleRegistry(IServiceScopeFactory serviceScopeFactory)
    {
        // check the bookings every minute for unpaid after 10 mins and set to abandoned
        Schedule(() => new BookingCleanupJob(serviceScopeFactory)).NonReentrant().ToRunNow().AndEvery(1).Minutes();

        // run the proc to clear out zombie rows once a day
        Schedule(() => new ZombieCleanupJob(serviceScopeFactory)).NonReentrant().ToRunEvery(1).Days();
    }
}

Then my job (in this case BookingCleanup) looks like this:

using System;
using System.Collections.Generic;
using System.Linq;

namespace MyCode.Code.Fluent_Scheduler;

public class BookingCleanupJob : IJob
{
    private IServiceScopeFactory serviceScopeFactory;

    public BookingCleanupJob(IServiceScopeFactory serviceScopeFactory)
    {
        this.serviceScopeFactory = serviceScopeFactory;
     }

    // executes the scheduled task
    public async void Execute()
    {
        using var serviceScope = serviceScopeFactory.CreateScope();
        ISqlDatabase _repository = serviceScope.ServiceProvider.GetService<ISqlDatabase>();
        IConfiguration _config = serviceScope.ServiceProvider.GetService<IConfiguration>();
        IWebHostEnvironment _env = serviceScope.ServiceProvider.GetService<IWebHostEnvironment>();
        ILogging _logging = serviceScope.ServiceProvider.GetService<ILogging>();

       try
       {
            const string sql = "; EXEC [dbo].[sp_SetAbandonedBookingsJob]";
            var AbandonedBookings = await _repository.GetListAsync<Booking>(sql) as List<Booking>;

           // logging example
            var desc = "sp_SetAbandonedBookingsJob was called and updated: " + AbandonedBookings.Count + " rows.";
            await _logging.LogAsync(Constants.LogType_BookingCleanupJobRUN, null, null, desc);

This has worked really well for me and the application has remained stable for several years now. I've tested upgrade to preview .Net 8 and it seemed fine.

hey Gus, not sure if you're still around but I was wondering if you are still running this and how it works for you?

I'm currently in the process of moving stuff around in my c# app I wrote about 2 years ago, and I was wanting to work towards moving fluentscheduler towards a dependency injection system and if it's possible to actually do it using version 5 of fluentscheduler (since it doesn't look like it's active anymore to be able to use V6)

Thanks!

-Joshua

GusBeare commented 10 months ago

Hi Joshua Yes, it works well for me. Not had any problems so far. I am using the latest Fluent Scheduler 5.5.1 with .Net 7. Cheers Gus

On Tue, 23 Jan 2024, 13:43 Rpgdudester, @.***> wrote:

I am not sure if this is ideal/correct or not but I use IServiceScopeFactory. I've an app running .Net 7 currently but that began with .Net 5. It's been stable through all versions and works very well. I realised that I need to pass several services into my jobs such as DB repo and logging and this is the way I found to do it.

In Program.cs.

using FluentScheduler; using Microsoft.Extensions.DependencyInjection; // IServiceScopeFactory

// Fluent scheduler IServiceScopeFactory serviceScopeFactory = app.Services.GetRequiredService(); JobManager.Initialize(new ScheduleRegistry(serviceScopeFactory));

Then Registry.cs.

using FluentScheduler; using Microsoft.Extensions.DependencyInjection;

namespace MyCode.Code.Fluent_Scheduler;

public class ScheduleRegistry : Registry { public ScheduleRegistry(IServiceScopeFactory serviceScopeFactory) { // check the bookings every minute for unpaid after 10 mins and set to abandoned Schedule(() => new BookingCleanupJob(serviceScopeFactory)).NonReentrant().ToRunNow().AndEvery(1).Minutes();

    // run the proc to clear out zombie rows once a day
    Schedule(() => new ZombieCleanupJob(serviceScopeFactory)).NonReentrant().ToRunEvery(1).Days();
}

}

Then my job (in this case BookingCleanup) looks like this:

using System; using System.Collections.Generic; using System.Linq;

namespace MyCode.Code.Fluent_Scheduler;

public class BookingCleanupJob : IJob { private IServiceScopeFactory serviceScopeFactory;

public BookingCleanupJob(IServiceScopeFactory serviceScopeFactory)
{
    this.serviceScopeFactory = serviceScopeFactory;
 }

// executes the scheduled task
public async void Execute()
{
    using var serviceScope = serviceScopeFactory.CreateScope();
    ISqlDatabase _repository = serviceScope.ServiceProvider.GetService<ISqlDatabase>();
    IConfiguration _config = serviceScope.ServiceProvider.GetService<IConfiguration>();
    IWebHostEnvironment _env = serviceScope.ServiceProvider.GetService<IWebHostEnvironment>();
    ILogging _logging = serviceScope.ServiceProvider.GetService<ILogging>();

   try
   {
        const string sql = "; EXEC [dbo].[sp_SetAbandonedBookingsJob]";
        var AbandonedBookings = await _repository.GetListAsync<Booking>(sql) as List<Booking>;

       // logging example
        var desc = "sp_SetAbandonedBookingsJob was called and updated: " + AbandonedBookings.Count + " rows.";
        await _logging.LogAsync(Constants.LogType_BookingCleanupJobRUN, null, null, desc);

This has worked really well for me and the application has remained stable for several years now. I've tested upgrade to preview .Net 8 and it seemed fine.

hey Gus, not sure if you're still around but I was wondering if you are still running this and how it works for you?

I'm currently in the process of moving stuff around in my c# app I wrote about 2 years ago, and I was wanting to work towards moving fluentscheduler towards a dependency injection system and if it's possible to actually do it using version 5 of fluentscheduler (since it doesn't look like it's active anymore to be able to use V6)

Thanks!

-Joshua

— Reply to this email directly, view it on GitHub https://github.com/fluentscheduler/FluentScheduler/issues/271#issuecomment-1906084115, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABHNLLOMMETEFFTHS76XEP3YP65BTAVCNFSM4MCXI6P2U5DIOJSWCZC7NNSXTN2JONZXKZKDN5WW2ZLOOQ5TCOJQGYYDQNBRGE2Q . You are receiving this because you commented.Message ID: @.***>