Data Imports in Optimizely: Part 7 - Separate jobs to be run independently

Often, importing data into Optimizely will involve the import of multiple different entities. For example, an import may need to handle articles, events and news. These will likely come from distinct data sources. It makes sense to logically separate these into different code to allow focusing on one at a time. You can take this further by allowing the jobs to be run independently, so that if one import is updated, you don’t need to run all of the imports again.

You can therefore logically separate out to components. One is the Optimizely schedule job which coordinates the running of the job. The other is the functionality that actually does the work.

Consider the following interface for a task that represents the functionality that does the work:

public interface ITask
{
    Task ExecuteAsync(CancellationToken cancellationToken);
}

We can then implement this interface and do our work:

public class MyTask1 : ITask
{
    public Task ExecuteAsync(CancellationToken cancellationToken)
    {
        return Task.FromResult("Task 1 completed successfully")
    }
}

public class MyTask2 : ITask
{
    public Task ExecuteAsync(CancellationToken cancellationToken)
    {
        return Task.FromResult("Task 2 completed successfully")
    }
}

We then need to register our classes with DI, either in an initialization module or from our Startup class:

context.Services.AddTransient<MyTask1>();
context.Services.AddTransient<MyTask2>();

Finally, we can create a job for each of the tasks, as well as one that runs them all for convenience:

using EPiServer.Scheduler;
using Nito.AsyncEx;

public class MyJob1 : ScheduledJobBase
{
    private readonly MyTask1 _myTask1;
    private CancellationTokenSource? _cancellationTokenSource;

    public MyJob1(MyTask1 myTask1)
    {
        _myTask1 = myTask1;
        IsStoppable = true;
    }

    public override void Stop()
    {
        _cancellationTokenSource?.Cancel();
    }

    public override string Execute()
    {
        _cancellationTokenSource = new CancellationTokenSource();

        return AsyncContext.Run(async () =>
        {
            return await _myTask1.ExecuteAsync(_cancellationTokenSource.Token);
        });
    }
}

public class MyJob2 : ScheduledJobBase
{
    private readonly MyTask2 _myTask2;
    private CancellationTokenSource? _cancellationTokenSource;

    public MyJob2(MyTask2 myTask2)
    {
        _myTask2 = myTask2;
        IsStoppable = true;
    }

    public override void Stop()
    {
        _cancellationTokenSource?.Cancel();
    }

    public override string Execute()
    {
        _cancellationTokenSource = new CancellationTokenSource();

        return AsyncContext.Run(async () =>
        {
            return await _myTask2.ExecuteAsync(_cancellationTokenSource.Token);
        });
    }
}

public class MyFullImportJob : ScheduledJobBase
{
    private readonly MyTask1 _myTask1;
    private readonly MyTask2 _myTask2;
    private CancellationTokenSource? _cancellationTokenSource;

    public MyFullImportJob(MyTask1 myTask1, MyTask2 myTask2)
    {
        _myTask1 = myTask1;
        _myTask2 = myTask2;
        IsStoppable = true;
    }

    public override void Stop()
    {
        _cancellationTokenSource?.Cancel();
    }

    public override string Execute()
    {
        _cancellationTokenSource = new CancellationTokenSource();

        return AsyncContext.Run(async () =>
        {
            string result = await _myTask1.ExecuteAsync(_cancellationTokenSource.Token);

            if (_cancellationTokenSource.Token.IsCancellationRequested)
            {
                return result;
            }

            result = await _myTask2.ExecuteAsync(_cancellationTokenSource.Token);

            return result;
        });
    }
}

We now have the ability to trigger our jobs independently, or for convenience we can run the whole import at once by triggering the full import job.

Next
Next

Data Imports in Optimizely: Part 6 - Parallelize - if it makes sense