IoC Task Scheduler Design with Simple Injector with code example recommendations
题
Hi I am using Simple Injector and I am trying to implement a scheduler service which calls command(s) at a programmable regular frequency. My design decisions are below:
1) I have a service that runs a background thread and resolves from the container ICommandHandler<SyncExternalDataCommand>.Handle(someParameter)
and calls the command handle method.
RegisterCommandAsBusinessUnitCronJob
will be called for each BusinessUnit with the ID passed as a parameter on each call. We could also implement RegistercommandAsCustomerContactCronJob
to loop through customer records.
Is it ok to design the scheduler in this way with a background thread?
2) I have tried to keep the coupling between the composition root container and the scheduler to a minimum.
An approach with Action helper delegates helps to keep the code in the composition root section as opposed to within the service. By using a different design, could this coupling be reduced further or is this coupling acceptable?
Code is below (it is yet to be made thread-safe), comments on the design decisions above and improvements/redesign are very welcome.
Thanks, Chris
Bootstrap of the container
void Register(Container container)
{
container.RegisterSingle<IUnitOfWorkFactory>(
new UnitOfWorkFactory(container));
container.RegisterLifetimeScope<IUnitOfWork, UnitOfWork>();
container.RegisterSingle<ILogger, Logger>();
container.RegisterSingle<ICronJobService, CronJobService>();
container.Register<ICommandHandler<SyncExternalDataCommand>,
SyncExternalDataCommandHandler>();
var cronJobService = container.GetInstance<ICronJobService>();
cronJobService.RegisterCommandAsBusinessUnitCronJob("* 1 * * *",
(Int32 id) =>
{
var command = new SyncExternalDataCommand()
{
businessUnitId = id
};
using (container.BeginLifetimeScope())
{
// handler must be resolved INSIDE the scope.
var handler =
container.GetInstance<ICommandHandler<SyncExternalDataCommand>>();
handler.Handle(command);
}
}
);
}
Scheduler service
// to be instantiated as a singleton by IoC container
public class CronJobService : ICronJobService
{
// this dictionary is for tasks that will be called with
// the business unit primary key (Int32).
private cronJobs = new Dictionary<Action<Int32>, string>();
public void RegisterCommandAsBusinessUnitCronJob(
string cronString, Action<Int32> commandFactory)
{
cronJobs.Add(commandFactory, cronString);
}
// this below is called when we are running a task
protected static bool RunCreateAndHandleThread(
object parameter)
{
var jobParameters = (ThreadJobParameters)parameter;
if (!cancellationTokenSource.IsCancellationRequested)
{
// we will need to construct new graph with a
// proxy command
jobParameters.helper(jobParameters.businessUnitId);
}
return true;
}
protected static void SomeBackgroundLoop(
RunHandlePollingLoopParameters parameters)
{
IUnitOfWork unitOfWork =
parameters.unitOfWorkFactory.CreateUnitOfWork();
using (unitOfWork)
{
var businessUnits =
unitOfWork.BusinessUnitRepository.Get();
// loop through cron jobs and business units
foreach (var helperFactory in parameters.cronJobs.Keys)
{
// its time to run this job...
if (isTimeToRunCronjob)
{
var jobParameters = new ThreadJobParameters
{
helper = helperFactory,
businessUnitId = businessUnit.Id
};
Task.Factory.StartNew<bool>(
RunCreateAndHandleThread,
jobParameters,
CronJobService.cancellationTokenSource.Token,
TaskCreationOptions.LongRunning,
TaskScheduler.Default);
}
}
}
}
}
解决方案
Is it ok to design the scheduler in this way with a background thread?
Sure. As long as everything is thread-safe. From a DI perspective there is no problem, but of course you need to make sure that shared dependencies are thread-safe (as you must do with all multi-threaded applications). However, when the service is performant / fast enough without multi-threading, make the application single-threaded. This makes everything so much easier.
By using a different design, could this coupling be reduced further or is this coupling acceptable?
Possibly. Your CronJobService
takes a dependency on the Container
. This is not a problem, as long as it is part of the Composition Root. However, your CronJobService
contains a lot of logic, which means you probably want to test it, but this is hard since you coupled it with the container and Task.Factory
. Separate this if you can.
The use of a IUnitOfWorkFactory
can be fine (take a look at this SO question and answer for instance). But since you already have a IUnitOfWork
registered Per Lifetime Scope, I don't really see how a IUnitOfWorkFactory
helps. So instead of calling the CreateUnitOfWork
inside the SomeBackgroundLoop
, you can just wrap this method with a using (container.BeginLifetimeScope())
. This way this method runs inside the context of a lifetime scope, which means that any injected IUnitOfWork
(within this thread) will be the same unit of work.