📄 src/Domain/Uploader.cs
using System;
using System.ComponentModel.DataAnnotations;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;

namespace Slopper.Domain;

public sealed class Uploader(
    IServiceProvider serviceProvider,
    IOptionsMonitor<UploaderOptions> options,
    TimeProvider timeProvider,
    IClipRepository clipRepository
)
{
    public async Task Upload(string platform, CancellationToken cancellationToken)
    {
        var uploader = serviceProvider.GetRequiredKeyedService<IUploader>(platform);

        var publishAt = timeProvider.GetUtcNow();
        var interval = options.CurrentValue.PublishInterval;
        await foreach (var clip in clipRepository.GetNotUploadedTo(platform, cancellationToken))
        {
            var upload = await uploader.Upload(clip, publishAt, cancellationToken);
            clip.Uploads.Add(upload);
            await clipRepository.Save(clip, cancellationToken);

            publishAt += interval;
        }
    }
}

public sealed class UploaderOptions
{
    [Required]
    public required TimeSpan PublishInterval { get; set; }
}

[OptionsValidator]
public sealed partial class UploaderOptionsValidator : IValidateOptions<UploaderOptions>;

public static class UploaderServiceCollectionExtensions
{
    extension(IServiceCollection services)
    {
        public IServiceCollection AddUploader()
        {
            services.AddOptions<UploaderOptions>().BindConfiguration("Uploader").ValidateOnStart();
            services.AddTransient<IValidateOptions<UploaderOptions>, UploaderOptionsValidator>();

            services.TryAddSingleton(TimeProvider.System);
            services.AddTransient<Uploader>();
            return services;
        }
    }
}