DEV Community

seonglinchua
seonglinchua

Posted on

Tutorial: Building a Configurable Scheduled Windows Service with Logging and Dependency Injection

This tutorial will guide you through creating a Windows Service in C# .NET that:

  • Schedules jobs to run at specific intervals.
  • Logs events and potential errors.
  • Is configurable through dependency injection.

Prerequisites:

  • Visual Studio: You'll need Visual Studio 2019 or later installed.
  • .NET SDK: Ensure you have the .NET SDK installed (comes with Visual Studio).

Steps:

1. Create a New Windows Service (.NET Framework) Project:

  • Open Visual Studio.
  • Click on "Create a new project."
  • Search for and select "Windows Service (.NET Framework)" (make sure it's the .NET Framework version).
  • Click "Next."
  • Give your project a name (e.g., ScheduledService) and choose a location.
  • Click "Create."

2. Install Necessary NuGet Packages:

We'll use the following packages for scheduling, logging, and dependency injection:

  • Microsoft.Extensions.DependencyInjection: For dependency injection.
  • Microsoft.Extensions.Logging: For logging.
  • Microsoft.Extensions.Configuration: For reading configuration.
  • Quartz.NET: For scheduling jobs.

Open the NuGet Package Manager (Tools -> NuGet Package Manager -> Manage NuGet Packages for Solution) and install these packages.

3. Define Configuration:

Let's create a configuration file to specify job schedules.

  • Right-click on your project in Solution Explorer.
  • Go to "Add" -> "New Item..."
  • Select "JSON File" and name it appsettings.json.
  • Add the following configuration structure (adjust the cron expressions as needed):

    {
      "Logging": {
        "LogLevel": {
          "Default": "Information",
          "Microsoft": "Warning",
          "Microsoft.Hosting.Lifetime": "Information"
        }
      },
      "JobSchedules": [
        {
          "JobName": "SampleJob",
          "CronExpression": "0/30 * * * * ?" // Run every 30 seconds
        },
        {
          "JobName": "AnotherJob",
          "CronExpression": "0 0 * * * ?"    // Run at the top of every hour
        }
      ]
    }
    
    • Logging: Standard .NET logging configuration.
    • JobSchedules: An array defining the jobs to be scheduled.
      • JobName: A unique name for the job.
      • CronExpression: A Quartz.NET cron expression defining the schedule.
  • In Solution Explorer, select appsettings.json and in the Properties window, set "Copy to Output Directory" to "Copy if newer."

4. Implement Logging:

Let's set up logging using Microsoft.Extensions.Logging.

  • Create a new folder named Logging in your project.
  • Create a new class named ServiceLogger.cs within the Logging folder:

    using Microsoft.Extensions.Logging;
    using System;
    
    public class ServiceLogger<T> : ILogger<T>
    {
        private readonly string _categoryName;
        private readonly Action<string> _logAction;
    
        public ServiceLogger(string categoryName, Action<string> logAction)
        {
            _categoryName = categoryName;
            _logAction = logAction;
        }
    
        public IDisposable BeginScope<TState>(TState state) => null; // Not needed for simple console logging
    
        public bool IsEnabled(LogLevel logLevel) => true; // Log all levels
    
        public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
        {
            string message = formatter(state, exception);
            if (!string.IsNullOrEmpty(message) || exception != null)
            {
                _logAction($"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [{logLevel}] {_categoryName}: {message} {exception}");
            }
        }
    }
    
    public class ServiceLoggerFactory : ILoggerFactory
    {
        private readonly Action<string> _logAction;
        private readonly List<IloggerProvider> _providers = new List<IloggerProvider>();
    
        public ServiceLoggerFactory(Action<string> logAction)
        {
            _logAction = logAction;
        }
    
        public void AddProvider(IloggerProvider provider)
        {
            _providers.Add(provider);
        }
    
        public ILogger CreateLogger(string categoryName) => new ServiceLogger<string>(categoryName, _logAction);
    
        public void Dispose()
        {
            foreach (var provider in _providers)
            {
                provider?.Dispose();
            }
        }
    }
    

    This provides a basic logger that writes to a specified Action<string>, which we'll configure to write to the Windows Event Log.

5. Define Jobs:

Create interfaces and implementations for your scheduled jobs.

  • Create a new folder named Jobs in your project.
  • Create an interface IJobService.cs:

    using System;
    using System.Threading.Tasks;
    
    public interface IJobService
    {
        string JobName { get; }
        Task ExecuteAsync(IServiceProvider serviceProvider);
    }
    
  • Create a sample job implementation SampleJob.cs:

    using Microsoft.Extensions.Logging;
    using System;
    using System.Threading.Tasks;
    
    namespace ScheduledService.Jobs
    {
        public class SampleJob : IJobService
        {
            public string JobName => "SampleJob";
            private readonly ILogger<SampleJob> _logger;
    
            public SampleJob(ILogger<SampleJob> logger)
            {
                _logger = logger;
            }
    
            public async Task ExecuteAsync(IServiceProvider serviceProvider)
            {
                _logger.LogInformation($"SampleJob executed at: {DateTime.Now}");
                // Add your actual job logic here
                await Task.Delay(1000); // Simulate some work
                _logger.LogInformation($"SampleJob finished at: {DateTime.Now}");
            }
        }
    }
    
  • Create another sample job implementation AnotherJob.cs:

    using Microsoft.Extensions.Logging;
    using System;
    using System.Threading.Tasks;
    
    namespace ScheduledService.Jobs
    {
        public class AnotherJob : IJobService
        {
            public string JobName => "AnotherJob";
            private readonly ILogger<AnotherJob> _logger;
    
            public AnotherJob(ILogger<AnotherJob> logger)
            {
                _logger = logger;
            }
    
            public async Task ExecuteAsync(IServiceProvider serviceProvider)
            {
                _logger.LogWarning($"AnotherJob executed at: {DateTime.Now}");
                // Add your actual job logic here
                await Task.Delay(2000); // Simulate different work
                _logger.LogWarning($"AnotherJob finished at: {DateTime.Now}");
            }
        }
    }
    

6. Configure Dependency Injection and Scheduling:

Modify the Service1.cs to set up dependency injection, logging, configuration, and the Quartz.NET scheduler.

  • Open Service1.cs.
  • Add the following using directives:

    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Logging;
    using Quartz;
    using Quartz.Impl;
    using ScheduledService.Jobs;
    using System;
    using System.Collections.Generic;
    using System.Collections.Specialized;
    using System.ServiceProcess;
    using System.Threading.Tasks;
    
  • Modify the Service1 class:

    public partial class Service1 : ServiceBase
    {
        private ServiceProvider _serviceProvider;
        private ILogger<Service1> _logger;
        private IScheduler _scheduler;
    
        public Service1()
        {
            InitializeComponent();
        }
    
        protected override void OnStart(string[] args)
        {
            ConfigureServices();
            InitializeScheduler().GetAwaiter().GetResult();
            _logger.LogInformation("Service started.");
        }
    
        protected override void OnStop()
        {
            _scheduler?.Shutdown().GetAwaiter().GetResult();
            _logger.LogInformation("Service stopped.");
        }
    
        private void ConfigureServices()
        {
            var configuration = new ConfigurationBuilder()
                .SetBasePath(AppDomain.CurrentDomain.BaseDirectory)
                .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
                .Build();
    
            var serviceCollection = new ServiceCollection();
    
            // Configure logging to Windows Event Log
            var eventLogName = "ScheduledServiceLog";
            if (!System.Diagnostics.EventLog.SourceExists(eventLogName))
            {
                System.Diagnostics.EventLog.CreateEventSource(eventLogName, "Application");
            }
            Action<string> logAction = message =>
                System.Diagnostics.EventLog.WriteEntry("Application", message, System.Diagnostics.EventLogEntryType.Information, 1);
            serviceCollection.AddSingleton<ILoggerFactory>(new ServiceLoggerFactory(logAction));
            serviceCollection.AddLogging(builder => builder.AddProvider(new CustomLoggerProvider(logAction)));
    
            // Add configuration
            serviceCollection.AddSingleton<IConfiguration>(configuration);
    
            // Register job services
            serviceCollection.AddTransient<SampleJob>();
            serviceCollection.AddTransient<AnotherJob>();
    
            _serviceProvider = serviceCollection.BuildServiceProvider();
            _logger = _serviceProvider.GetRequiredService<ILogger<Service1>>();
        }
    
        private async Task InitializeScheduler()
        {
            try
            {
                NameValueCollection props = new NameValueCollection
                {
                    { "quartz.serializer.type", "binary" }
                };
                StdSchedulerFactory factory = new StdSchedulerFactory(props);
                _scheduler = await factory.GetScheduler();
                _scheduler.JobFactory = new DependencyInjectionJobFactory(_serviceProvider);
                await _scheduler.Start();
    
                var jobSchedules = _serviceProvider.GetRequiredService<IConfiguration>()
                    .GetSection("JobSchedules")
                    .Get<List<JobScheduleConfig>>();
    
                if (jobSchedules != null)
                {
                    foreach (var scheduleConfig in jobSchedules)
                    {
                        var jobType = Type.GetType($"ScheduledService.Jobs.{scheduleConfig.JobName}, ScheduledService");
                        if (jobType != null && typeof(IJobService).IsAssignableFrom(jobType))
                        {
                            var jobKey = new JobKey(scheduleConfig.JobName);
                            if (!await _scheduler.CheckExists(jobKey))
                            {
                                var jobDetail = JobBuilder.Create(jobType)
                                    .WithIdentity(jobKey)
                                    .StoreDurably()
                                    .Build();
    
                                var trigger = TriggerBuilder.Create()
                                    .WithIdentity($"{scheduleConfig.JobName}-trigger")
                                    .WithCronSchedule(scheduleConfig.CronExpression)
                                    .ForJob(jobDetail)
                                    .Build();
    
                                await _scheduler.AddJob(jobDetail, true);
                                await _scheduler.ScheduleJob(trigger);
                                _logger.LogInformation($"Job '{scheduleConfig.JobName}' scheduled with cron expression: '{scheduleConfig.CronExpression}'.");
                            }
                            else
                            {
                                _logger.LogWarning($"Job with name '{scheduleConfig.JobName}' already exists.");
                            }
                        }
                        else
                        {
                            _logger.LogError($"Could not find or load job type: 'ScheduledService.Jobs.{scheduleConfig.JobName}'.");
                        }
                    }
                }
                else
                {
                    _logger.LogWarning("No job schedules found in configuration.");
                }
            }
            catch (Exception ex)
            {
                _logger.LogError($"Error initializing scheduler: {ex.Message}");
                if (_scheduler != null && _scheduler.IsStarted)
                {
                    await _scheduler.Shutdown(true);
                }
                throw; // Re-throw to prevent service from starting incorrectly
            }
        }
    }
    
    public class JobScheduleConfig
    {
        public string JobName { get; set; }
        public string CronExpression { get; set; }
    }
    
    public class DependencyInjectionJobFactory : IJobFactory
    {
        private readonly IServiceProvider _serviceProvider;
    
        public DependencyInjectionJobFactory(IServiceProvider serviceProvider)
        {
            _serviceProvider = serviceProvider;
        }
    
        public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
        {
            return _serviceProvider.GetRequiredService(bundle.JobDetail.JobType) as IJob;
        }
    
        public void ReturnJob(IJob job) { }
    }
    
    public class CustomLoggerProvider : ILoggerProvider
    {
        private readonly Action<string> _logAction;
    
        public CustomLoggerProvider(Action<string> logAction)
        {
            _logAction = logAction;
        }
    
        public ILogger CreateLogger(string categoryName) => new ServiceLogger<string>(categoryName, _logAction);
    
        public void Dispose() { }
    }
    

7. Install the Windows Service:

To run the service, you need to install it. You can use the InstallUtil.exe tool that comes with the .NET Framework.

  • Open the Visual Studio Developer Command Prompt (as Administrator).
  • Navigate to the bin/Debug or bin/Release folder of your service project.
  • Run the following command:

    InstallUtil.exe ScheduledService.exe
    
  • You should see output indicating that the service was successfully installed.

8. Manage and Run the Service:

  • Open the Services Manager (search for "Services" in the Start Menu).
  • You should see your service listed (e.g., "ScheduledService").
  • Right-click on the service and select "Start."
  • Check the Windows Event Log (Event Viewer) under "Application" for the log messages from your service and the executed jobs.

9. Configure Job Schedules:

To change the job schedules, simply modify the appsettings.json file. The service is configured to reload this file on change (though the Quartz.NET scheduler won't dynamically update running jobs in this basic implementation). You would typically need to restart the service for schedule changes to take effect.

Further Enhancements:

  • Dynamic Job Management: Implement a mechanism to dynamically add, remove, or modify jobs without restarting the service (this would involve more complex scheduling logic and potentially storing job configurations in a persistent store).
  • More Robust Logging: Use a more advanced logging library like Serilog or NLog for richer logging features (e.g., file rolling, different output formats, structured logging).
  • Error Handling and Retries: Implement more sophisticated error handling within jobs, including retry mechanisms for transient failures.
  • Job Data: Allow passing data to jobs through the configuration.
  • Health Checks: Implement health checks to monitor the status of the service and scheduled jobs.

This tutorial provides a solid foundation for building configurable and scheduled Windows Services using modern .NET practices like dependency injection and logging. Remember to adapt and expand upon this based on your specific requirements.

Top comments (0)