I will show how to build a production‑ready console/worker app on .NET using the Generic Host: predictable configuration (JSON + environment + user‑secrets), structured logging (console + optional Windows Event Log), dependency injection, and a background worker. A complete, runnable sample is provided.

🔗 Sample Code: The complete sample project demonstrates all concepts covered in this article. You can download and run it to see everything working together.

📁 GitHub Repository: https://github.com/NickolaiA/Blog_DotNET_Console-Worker-App

Why I use the Generic Host

The .NET Generic Host unifies app startup and lifetime management across app types. By using Host.CreateDefaultBuilder, I get configuration from JSON, per‑environment JSON, user secrets (in Development), environment variables, and command‑line arguments — plus logging providers and DI — out of the box.

Graceful shutdown: It also handles Ctrl+C/TERM and graceful shutdown for background tasks.

Configuration that scales from Dev to Prod

I read configuration from appsettings.json and appsettings.<Environment>.json, overlay Development‑only values with User Secrets, and finally overlay with environment variables. For console/worker apps I set DOTNET_ENVIRONMENT (for example Development, Staging, or Production).

# PowerShell
$env:DOTNET_ENVIRONMENT = "Development"
dotnet run

# Bash
export DOTNET_ENVIRONMENT=Development
dotnet run

For sensitive values (like SMTP password) in Development:

dotnet user-secrets init
dotnet user-secrets set "AppSettings:Smtp:Password" "your-dev-password"

Logging: console timestamps and Windows Event Log

I configure console logging with the built‑in Simple formatter and enable UTC timestamps. On Windows I optionally add the Event Log provider — if a custom SourceName is used, I ensure the source exists (creating it once requires administrator rights).

{
  "Logging": {
    "LogLevel": { "Default": "Information" },
    "Console": {
      "FormatterName": "simple",
      "FormatterOptions": {
        "TimestampFormat": "[yyyy-MM-ddTHH:mm:ssK] ",
        "UseUtcTimestamp": true
      }
    },
    "EventLog": {
      "SourceName": "MyWorker",
      "LogName": "Application"
    }
  }
}

Dependency Injection and options

I bind the AppSettings section to a POCO and inject via IOptions<AppSettings>. This keeps configuration usage clean, testable, and ready for validation.

public sealed class AppSettings
{
    public SmtpSettings Smtp { get; set; } = new();
    public string TestRecipient { get; set; } = string.Empty;

    public sealed class SmtpSettings
    {
        public string Host { get; set; } = string.Empty;
        public int Port { get; set; } = 587;
        public bool UseSsl { get; set; } = true;
        public string Username { get; set; } = string.Empty;
        public string Password { get; set; } = string.Empty; // user-secrets/env
        public string From { get; set; } = string.Empty;
    }
}

Background worker

The sample implements a BackgroundService that logs startup, optionally sends a test email, then exits. This demonstrates clean use of ILogger<T> and IOptions<T> in a hosted service.

public sealed class App : BackgroundService
{
    private readonly ILogger<App> _logger;
    private readonly IEmailService _email;
    private readonly AppSettings _settings;

    public App(ILogger<App> logger, IEmailService email, IOptions<AppSettings> settings)
    {
        _logger = logger; _email = email; _settings = settings.Value;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Worker starting in {{Environment}}",
            Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? "Production");

        if (!string.IsNullOrWhiteSpace(_settings.TestRecipient))
        {
            try
            {
                await _email.SendAsync(_settings.Smtp.From, _settings.TestRecipient,
                    "Hello from worker", "This is a test email sent by the worker.");
                _logger.LogInformation("Test email sent to {{Recipient}}", _settings.TestRecipient);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Failed to send test email");
            }
        }
        else
        {
            _logger.LogWarning("No TestRecipient configured; skipping email send.");
        }

        _logger.LogInformation("Worker finished; shutting down.");
    }
}

How to run the sample

First, get the sample code from GitHub:

git clone https://github.com/NickolaiA/Blog_DotNET_Console-Worker-App.git
cd Blog_DotNET_Console-Worker-App

Then follow these steps:

  1. Set DOTNET_ENVIRONMENT=Development.
  2. Set SMTP password with user‑secrets.
  3. dotnet build then dotnet run --project ./src/WorkerSample/WorkerSample.csproj.
  4. (Windows) If using Event Log with a custom source, run once as Administrator to create the source.

Key source files

  • Program.cs – Host setup, console logging, optional EventLog, DI registrations.
  • App.cs – The BackgroundService worker.
  • AppSettings.cs – Strongly‑typed options model.
  • IEmailService + SmtpClientWrapper – Minimal SMTP abstraction for demo.
  • appsettings.json + appsettings.Development.json – Logging/SMTP configuration.
💡 Key Takeaways:
  • Use Host.CreateDefaultBuilder for consistent configuration and logging setup
  • Leverage user secrets for development-only sensitive values
  • Implement proper structured logging with timestamps
  • Use IOptions<T> for clean configuration injection
  • Build testable services with proper dependency injection

References