Building an ASP.NET Core Windows Service Task Scheduler

Introduction


Source Code


Creating Our Perfect Scheduler

No Windows Service Project Template
.Net Core Console Application

Creating an Asp.Net Console Application

Asp.Net Core Windows Service Dependencies

Write the Service

public interface IHostedService
    {
        //
        // Summary:
        //     Triggered when the application host is ready to start the service.
        //
        // Parameters:
        //   cancellationToken:
        //     Indicates that the start process has been aborted.
        Task StartAsync(CancellationToken cancellationToken);
        //
        // Summary:
        //     Triggered when the application host is performing a graceful shutdown.
        //
        // Parameters:
        //   cancellationToken:
        //     Indicates that the shutdown process should no longer be graceful.
        Task StopAsync(CancellationToken cancellationToken);
    }

StartAsync

public async Task StartAsync(CancellationToken cancellationToken)
        {
            try
            {
                var scheduler = await GetScheduler();
                var serviceProvider = GetConfiguredServiceProvider();
                scheduler.JobFactory = new CustomJobFactory(serviceProvider);

                await scheduler.Start();
                await scheduler.ScheduleJob(GetDailyJob(), GetDailyJobTrigger());
                await Task.Delay(1000);
                await scheduler.ScheduleJob(GetWeeklyJob(), GetWeeklyJobTrigger());
                await Task.Delay(1000);
                await scheduler.ScheduleJob(GetMonthlyJob(), GetMonthlyJobTrigger());
                await Task.Delay(1000);
            }
            catch (Exception ex)
            {
                throw new CustomConfigurationException(ex.Message);
            }
        }
private static async Task<IScheduler> GetScheduler()
        {
            var props = new NameValueCollection { { "quartz.serializer.type", "binary" } };
            var factory = new StdSchedulerFactory(props);
            var scheduler = await factory.GetScheduler();
            return scheduler;
        }
private IServiceProvider GetConfiguredServiceProvider()
        {
            var services = new ServiceCollection()
                .AddScoped<IDailyJob, DailyJob>()
                .AddScoped<IWeeklyJob, WeeklyJob>()
                .AddScoped<IMonthlyJob, MonthlyJob>()
                .AddScoped<IHelperService, HelperService>();
            return services.BuildServiceProvider();
        }
#region "Private Functions"
        private IJobDetail GetDailyJob()
        {
            return JobBuilder.Create<IDailyJob>()
                .WithIdentity("dailyjob", "dailygroup")
                .Build();
        }
        private ITrigger GetDailyJobTrigger()
        {
            return TriggerBuilder.Create()
                 .WithIdentity("dailytrigger", "dailygroup")
                 .StartNow()
                 .WithSimpleSchedule(x => x
                     .WithIntervalInHours(24)
                     .RepeatForever())
                 .Build();
        }
        private IJobDetail GetWeeklyJob()
        {
            return JobBuilder.Create<IWeeklyJob>()
                .WithIdentity("weeklyjob", "weeklygroup")
                .Build();
        }
        private ITrigger GetWeeklyJobTrigger()
        {
            return TriggerBuilder.Create()
                 .WithIdentity("weeklytrigger", "weeklygroup")
                 .StartNow()
                 .WithSimpleSchedule(x => x
                     .WithIntervalInHours(120)
                     .RepeatForever())
                 .Build();
        }
        private IJobDetail GetMonthlyJob()
        {
            return JobBuilder.Create<IMonthlyJob>()
                .WithIdentity("monthlyjob", "monthlygroup")
                .Build();
        }
        private ITrigger GetMonthlyJobTrigger()
        {
            return TriggerBuilder.Create()
                 .WithIdentity("monthlytrigger", "monthlygroup")
                 .StartNow()
                 .WithSimpleSchedule(x => x
                     .WithIntervalInHours(720)
                     .RepeatForever())
                 .Build();
        }
        #endregion

StopAsync

public Task StopAsync(CancellationToken cancellationToken)
        {
            return Task.CompletedTask;
        }

Creating the JobBuilders and TriggerBuilders

IDailyJob

using Quartz;

namespace Backup.Service.Helpers.Interfaces
{
    public interface IDailyJob: IJob { }
}

IWeeklyJob

using Quartz;

namespace Backup.Service.Helpers.Interfaces
{
    public interface IWeeklyJob : IJob { }
}

IMonthlyJob

using Quartz;

namespace Backup.Service.Helpers.Interfaces
{
    public interface IMonthlyJob : IJob { }
}

IHelperService

using System.Threading.Tasks;

namespace Backup.Service.Helpers.Interfaces
{
    public interface IHelperService
    {
        Task PerformService(string schedule);
    }
}

Create a Custom Job Factory

using Quartz;
using Quartz.Spi;
using System;

namespace Backup.Service.Helpers
{
    public class CustomJobFactory : IJobFactory
    {
        protected readonly IServiceProvider _serviceProvider;
        public CustomJobFactory(IServiceProvider serviceProvider)
        {
            _serviceProvider = serviceProvider;
        }
        public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
        {
            try
            {
                return _serviceProvider.GetService(bundle.JobDetail.JobType) as IJob;
            }
            catch (Exception ex)
            {
                throw new CustomConfigurationException(ex.Message);
            }            
        }

        public void ReturnJob(IJob job)
        {
            var obj = job as IDisposable;
            obj?.Dispose();
        }
    }
}

Implement the Job Builder Interfaces

DailyJob

using Backup.Service.Helpers.Interfaces;
using Quartz;
using System.Threading.Tasks;

namespace Backup.Service.Helpers
{
    public class DailyJob : IDailyJob
    {
        public IHelperService _helperService;
        public DailyJob(IHelperService helperService)
        {
            _helperService = helperService;
        }
        public async Task Execute(IJobExecutionContext context)
        {
            await _helperService.PerformService(BackupSchedule.Daily);
        }
    }
}

WeeklyJob

using Backup.Service.Helpers.Interfaces;
using Quartz;
using System.Threading.Tasks;

namespace Backup.Service.Helpers
{
    public class WeeklyJob : IWeeklyJob
    {
        public IHelperService _helperService;
        public WeeklyJob(IHelperService helperService)
        {
            _helperService = helperService;
        }
        public async Task Execute(IJobExecutionContext context)
        {
            await _helperService.PerformService(BackupSchedule.Weekly);
        }
    }
}

MonthlyJob

using Backup.Service.Helpers.Interfaces;
using Quartz;
using System.Threading.Tasks;

namespace Backup.Service.Helpers
{
    public class MonthlyJob : IMonthlyJob
    {
        public IHelperService _helperService;
        public MonthlyJob(IHelperService helperService)
        {
            _helperService = helperService;
        }
        public async Task Execute(IJobExecutionContext context)
        {
            await _helperService.PerformService(BackupSchedule.Monthly);
        }
    }
}

Implement HelperService

Configure NLog

<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

  <targets>
    <target name="logfile" xsi:type="File" fileName="backupclientlogfile.txt" />
    <target name="logconsole" xsi:type="Console" />
  </targets>

  <rules>
    <logger name="*" minlevel="Info" writeTo="logconsole" />
    <logger name="*" minlevel="Debug" writeTo="logfile" />
  </rules>
</nlog>
private void SetUpNLog()
        {
            var config = new NLog.Config.LoggingConfiguration();

            // Targets where to log to: File and Console
            var logfile = new NLog.Targets.FileTarget("logfile") { FileName = "backupclientlogfile.txt" };
            var logconsole = new NLog.Targets.ConsoleTarget("logconsole");

            // Rules for mapping loggers to targets            
            config.AddRule(NLog.LogLevel.Info, NLog.LogLevel.Fatal, logconsole);
            config.AddRule(NLog.LogLevel.Info, NLog.LogLevel.Fatal, logfile);

            // Apply config           
            LogManager.Configuration = config;

            _logger = LogManager.GetCurrentClassLogger();
        }
using Microsoft.Azure.Storage;
using Microsoft.Azure.Storage.Blob;
using Backup.Service.Helpers.Interfaces;
using System;
using System.Configuration;
using System.IO;
using System.IO.Compression;
using System.Threading.Tasks;
using NLog;

namespace Backup.Service.Helpers
{
    public class HelperService : IHelperService
    {
        private static ILogger _logger;
        public HelperService()
        {
            SetUpNLog();
        }

        private string FolderToZipLocation
        {
            get
            {
                var folderToZip = Convert.ToString(ConfigurationManager.AppSettings["FolderToZipLocation"]);
                if (string.IsNullOrWhiteSpace(folderToZip))
                {
                    throw new CustomConfigurationException(CustomConstants.NoFolderToZipLocationSettings);
                }
                return folderToZip;
            }
        }

        private string FolderFromZipLocation
        {
            get
            {
                var folderToZip = Convert.ToString(ConfigurationManager.AppSettings["FolderFromZipLocation"]);
                if (string.IsNullOrWhiteSpace(folderToZip))
                {
                    throw new CustomConfigurationException(CustomConstants.NoFolderFromZipLocationSettings);
                }
                return folderToZip;
            }
        }

        private string StorageConnectionString
        {
            get
            {
                var folderToZip = Convert.ToString(ConfigurationManager.ConnectionStrings["StorageConnectionString"]);
                if (string.IsNullOrWhiteSpace(folderToZip))
                {
                    throw new CustomConfigurationException(CustomConstants.NoStorageConnectionStringSettings);
                }
                return folderToZip;
            }
        }

        public async Task PerformService(string schedule)
        {
            try
            {
                _logger.Info($"{DateTime.Now}: The PerformService() is called with {schedule} schedule");
                var fileName = $"{DateTime.Now.Day}_{DateTime.Now.Month}_{DateTime.Now.Year}_{schedule}_backup.zip";
                var path = $"{FolderToZipLocation}\\{fileName}";
                if (!string.IsNullOrWhiteSpace(schedule))
                {
                    ZipTheFolder(path);
                    await UploadToAzureBlobStorage(schedule, path, fileName);
                    _logger.Info($"{DateTime.Now}: The PerformService() is finished with {schedule} schedule");
                }
            }
            catch (Exception ex)
            {
                _logger.Error($"{DateTime.Now}: Exception is occured at PerformService(): {ex.Message}");
                throw new CustomConfigurationException(ex.Message);
            }
        }

        private async Task UploadToAzureBlobStorage(string schedule, string path, string fileName)
        {
            try
            {
                if (CloudStorageAccount.TryParse(StorageConnectionString, out CloudStorageAccount cloudStorageAccount))
                {
                    _logger.Info($"{DateTime.Now}: The UploadToAzureBlobStorage() is called with {schedule} schedule");
                    var cloudBlobClient = cloudStorageAccount.CreateCloudBlobClient();
                    var cloudBlobContainer = cloudBlobClient.GetContainerReference(schedule);
                    var cloudBlockBlob = cloudBlobContainer.GetBlockBlobReference(fileName);
                    await cloudBlockBlob.UploadFromFileAsync(path);
                    _logger.Info($"{DateTime.Now}: The file is been uplaoded to the blob with {schedule} schedule");
                }
                else
                {
                    _logger.Error($"{DateTime.Now}: {CustomConstants.NoStorageConnectionStringSettings}");
                    throw new CustomConfigurationException(CustomConstants.NoStorageConnectionStringSettings);
                }
            }
            catch (Exception ex)
            {
                _logger.Error($"{DateTime.Now}: Exception is occured at UploadToAzureBlobStorage(): {ex.Message}");
                throw new CustomConfigurationException($"Error when uploading to blob: {ex.Message}");
            }
        }

        private void ZipTheFolder(string path)
        {
            try
            {
                _logger.Info($"{DateTime.Now}: The ZipTheFolder() is called ");
                if (File.Exists(path))
                {
                    File.Delete(path);
                    _logger.Info($"{DateTime.Now}: The file with the same name exists, thus deleted the same");
                }
                ZipFile.CreateFromDirectory(FolderFromZipLocation, path);
                _logger.Info($"{DateTime.Now}: The Zip file is been created");
            }
            catch (Exception ex)
            {
                _logger.Error($"{DateTime.Now}: Exception is occured at ZipTheFolder(): {ex.Message}");
                throw new CustomConfigurationException($"Error when zip: {ex.Message}");
            }

        }

        private void SetUpNLog()
        {
            var config = new NLog.Config.LoggingConfiguration();

            // Targets where to log to: File and Console
            var logfile = new NLog.Targets.FileTarget("logfile") { FileName = "backupclientlogfile.txt" };
            var logconsole = new NLog.Targets.ConsoleTarget("logconsole");

            // Rules for mapping loggers to targets            
            config.AddRule(NLog.LogLevel.Info, NLog.LogLevel.Fatal, logconsole);
            config.AddRule(NLog.LogLevel.Info, NLog.LogLevel.Fatal, logfile);

            // Apply config           
            LogManager.Configuration = config;

            _logger = LogManager.GetCurrentClassLogger();
        }
    }
}
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
  </startup>
  <appSettings>
    <add key="FolderToZipLocation" value="C:\BackupZip"/>
    <add key="FolderFromZipLocation" value="C:\Backup"/>
  </appSettings>
  <connectionStrings>
    <add name="StorageConnectionString" connectionString=""/>
  </connectionStrings>
</configuration>

Setting Up The Program

using Backup.Service.Extensions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
namespace Backup.Service
{
    public class Program
    {
        static async Task Main(string[] args)
        {
            var isDebugging = !(Debugger.IsAttached || args.Contains("--console"));
            var hostBuilder = new HostBuilder()
                .ConfigureServices((context, services) =>
                {
                    services.AddHostedService<BackupService>();
                });
            if (isDebugging)
            {
                await hostBuilder.RunTheServiceAsync();
            }
            else
            {
                await hostBuilder.RunConsoleAsync();
            }
        }
    }
}
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System.Threading;
using System.Threading.Tasks;

namespace Backup.Service.Extensions
{
    public static class ServiceBaseLifetimeExtension
    {
        public static IHostBuilder UseServiceLifetime(this IHostBuilder hostBuilder)
        {
            return hostBuilder.ConfigureServices((hostContext, services) => services.AddSingleton<IHostLifetime, ServiceLifeTime>());
        }

        public static Task RunTheServiceAsync(this IHostBuilder hostBuilder, CancellationToken cancellationToken = default)
        {
            return hostBuilder.UseServiceLifetime().Build().RunAsync(cancellationToken);
        }
    }
}
using Microsoft.Extensions.Hosting;
using System;
using System.ServiceProcess;
using System.Threading;
using System.Threading.Tasks;

namespace Backup.Service
{
    public class ServiceLifeTime : ServiceBase, IHostLifetime
    {
        public ServiceLifeTime(IApplicationLifetime applicationLifeTime)
        {
            ApplicationLifeTime = applicationLifeTime ?? throw new ArgumentNullException(nameof(applicationLifeTime));
            DelayStart = new TaskCompletionSource<object>();
        }

        public IApplicationLifetime ApplicationLifeTime { get; }
        public TaskCompletionSource<object> DelayStart { get; set; }

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

        protected override void OnStart(string[] args)
        {
            DelayStart.TrySetResult(null);
            base.OnStart(args); 
        }

        protected override void OnStop()
        {
            ApplicationLifeTime.StopApplication();
            base.OnStop();  
        }

        public Task WaitForStartAsync(CancellationToken cancellationToken)
        {
            cancellationToken.Register(() => DelayStart.TrySetCanceled());
            ApplicationLifeTime.ApplicationStopping.Register(Stop);
            new Thread(Run).Start();
            return DelayStart.Task;
        }

        private void Run()
        {
            try
            {
                Run(this);
                DelayStart.TrySetException(new InvalidOperationException("Stopped, without starting the service"));
            }
            catch (Exception ex)
            {
                DelayStart.TrySetException(ex);
            }
        }
    }
}

Creating the Windows Service

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp2.1</TargetFramework>
    <RuntimeIdentifier>win7-x64</RuntimeIdentifier>
    <SelfContained>true</SelfContained>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Azure.Storage.Blob" Version="10.0.3" />
    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.2.0" />
    <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="2.2.0" />
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="2.2.0" />
    <PackageReference Include="NLog" Version="4.6.6" />
    <PackageReference Include="Quartz" Version="3.0.7" />
    <PackageReference Include="System.ServiceProcess.ServiceController" Version="4.5.0" />
  </ItemGroup>

</Project>

Create the Release Configuration

Dotnet Publish Release

Install the Service

sc create BackupService binPath="C:\Users\SibeeshVenu\source\repos\PerfectScheduler\PerfectScheduler\Backup.Service\bin\release\netcoreapp2.1\win7-x64\Backup.Service.exe" start=delayed-auto

And then you can start the service by running the command as sc start BackupService. This should start your service.


Giving Permission to the Folders

Setting Permission for the Users

Output

2019-08-11 21:23:22.1088|INFO|Backup.Service.Helpers.HelperService|8/11/2019 9:23:22 PM: The PerformService() is called with daily schedule
2019-08-11 21:23:22.2497|INFO|Backup.Service.Helpers.HelperService|8/11/2019 9:23:22 PM: The ZipTheFolder() is called 
2019-08-11 21:23:22.2497|INFO|Backup.Service.Helpers.HelperService|8/11/2019 9:23:22 PM: The file with the same name exists, thus deleted the same
2019-08-11 21:23:22.2497|INFO|Backup.Service.Helpers.HelperService|8/11/2019 9:23:22 PM: The Zip file is been created
2019-08-11 21:23:22.2895|INFO|Backup.Service.Helpers.HelperService|8/11/2019 9:23:22 PM: The UploadToAzureBlobStorage() is called with daily schedule
2019-08-11 21:23:22.9013|INFO|Backup.Service.Helpers.HelperService|8/11/2019 9:23:22 PM: The file is been uplaoded to the blob with daily schedule
2019-08-11 21:23:22.9061|INFO|Backup.Service.Helpers.HelperService|8/11/2019 9:23:22 PM: The PerformService() is finished with daily schedule
2019-08-11 21:23:23.0116|INFO|Backup.Service.Helpers.HelperService|8/11/2019 9:23:23 PM: The PerformService() is called with weekly schedule
2019-08-11 21:23:23.0265|INFO|Backup.Service.Helpers.HelperService|8/11/2019 9:23:23 PM: The ZipTheFolder() is called 
2019-08-11 21:23:23.0418|INFO|Backup.Service.Helpers.HelperService|8/11/2019 9:23:23 PM: The file with the same name exists, thus deleted the same
2019-08-11 21:23:23.0555|INFO|Backup.Service.Helpers.HelperService|8/11/2019 9:23:23 PM: The Zip file is been created
2019-08-11 21:23:23.0618|INFO|Backup.Service.Helpers.HelperService|8/11/2019 9:23:23 PM: The UploadToAzureBlobStorage() is called with weekly schedule
2019-08-11 21:23:23.1519|INFO|Backup.Service.Helpers.HelperService|8/11/2019 9:23:23 PM: The file is been uplaoded to the blob with weekly schedule
2019-08-11 21:23:23.1565|INFO|Backup.Service.Helpers.HelperService|8/11/2019 9:23:23 PM: The PerformService() is finished with weekly schedule
2019-08-11 21:23:24.0175|INFO|Backup.Service.Helpers.HelperService|8/11/2019 9:23:24 PM: The PerformService() is called with monthly schedule
2019-08-11 21:23:24.0329|INFO|Backup.Service.Helpers.HelperService|8/11/2019 9:23:24 PM: The ZipTheFolder() is called 
2019-08-11 21:23:24.0461|INFO|Backup.Service.Helpers.HelperService|8/11/2019 9:23:24 PM: The file with the same name exists, thus deleted the same
2019-08-11 21:23:24.0461|INFO|Backup.Service.Helpers.HelperService|8/11/2019 9:23:24 PM: The Zip file is been created
2019-08-11 21:23:24.0633|INFO|Backup.Service.Helpers.HelperService|8/11/2019 9:23:24 PM: The UploadToAzureBlobStorage() is called with monthly schedule
2019-08-11 21:23:24.1239|INFO|Backup.Service.Helpers.HelperService|8/11/2019 9:23:24 PM: The file is been uplaoded to the blob with monthly schedule
2019-08-11 21:23:24.1239|INFO|Backup.Service.Helpers.HelperService|8/11/2019 9:23:24 PM: The PerformService() is finished with monthly schedule

Anjali Punjab

Anjali Punjab is a freelance writer, blogger, and ghostwriter who develops high-quality content for businesses. She is also a HubSpot Inbound Marketing Certified and Google Analytics Qualified Professional.