Here’s what I’ll use as the starting point for any new console application that I build. It includes simple logging to the console and to a file, dependency injection, and strongly typed configuration, including user secrets that don’t go to source control.
Main.cs
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using MyApplication.Configuration; using Serilog; using Serilog.Debugging; using System.Diagnostics; // Writes Serilog errors do the debug window in case you're having trouble // with your configuration. SelfLog.Enable(msg => Debug.WriteLine(msg)); await Host.CreateDefaultBuilder(args) .ConfigureAppConfiguration((hostContext, builder) => { // Allows you to use the secrets.json file in // your user directory during development, so you don't // risk putting sensitive information into source control builder.AddUserSecrets<Program>(); }) .ConfigureServices((hostContext, services) => { // Pulls the configuration information for Serilog // logging into the default Serilog logger Log.Logger = new LoggerConfiguration() .ReadFrom.Configuration(hostContext.Configuration) .CreateLogger(); // Replaces the default logger with Serilog services.AddLogging(builder => { builder.ClearProviders(); builder.AddSerilog(); }); services.Configure<Config>(hostContext.Configuration.GetSection("ConfigStuff")); services.AddSingleton<Program>(); }) .Build() .Services.GetRequiredService<Program>() .ExecuteAsync();
Program.cs
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using MyApplication.Configuration; using System.Text.Json; partial class Program { private readonly ILogger<Program> _logger; private readonly Config _config; // Injected logger and Config object public Program(ILogger<Program> logger, IOptions<Config> config) { _logger = logger; _config = config.Value; } public async Task ExecuteAsync() { _logger.LogDebug("Starting..."); _logger.LogDebug("Config data: {configJson}", JsonSerializer.Serialize(_config)); // Your real work goes here, probably a call to another class await DoSomethingAsync(); } private Task DoSomethingAsync() { _logger.LogInformation(@"Your API password is {apiPassword}", _config.ApiPassword); _logger.LogInformation(@"Your item is {itemID}/{itemName}.", _config.Item.ID, _config.Item.Name); _logger.LogInformation(@"You have {numWidgets} widgets in your collection", _config.Collection.Length); return Task.CompletedTask; } }
appsettings.json
{ "ConfigStuff": { "Item": { "ID": 34, "Name": "Foo" }, "Collection": [ { "Color": "Blue", "Shape": "Circle" }, { "Color": "Brown", "Shape": "Square" } ], "ApiPassword": "This is overridden from the secrets.json file" }, "Serilog": { "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ], "MinimumLevel": "Debug", "WriteTo": [ { "Name": "Console", "Args": { "restrictedToMinimumLevel": "Debug" } }, { "Name": "File", "Args": { "path": "C:/temp/log.txt", "rollingInterval": "Day", "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level}] [{SourceContext}] {Message}{NewLine}{Exception}", "restrictedToMinimumLevel": "Information" } } ] } }
secrets.json (in your user directory, not in the project directory)
{ "ConfigStuff": { "ApiPassword": "mypassword" } }
MyApplication.csproj
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net6.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <!-- Forces you to deal with compiler warnings --> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> <WarningsAsErrors /> <!-- Auto-generated when you open "Manage User Secrets" from the VS project --> <UserSecretsId>a6732cb3-823b-4d05-818e-53a24fb12bb8</UserSecretsId> <!-- These get injected into your generated AssemblyInfo class --> <Version>2.0.0</Version> <AssemblyVersion>2.0.0</AssemblyVersion> <Company>My Company Name</Company> <Product>My Product Name</Product> <AssemblyTitle>My Title</AssemblyTitle> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="6.0.1" /> <PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" /> <PackageReference Include="Serilog" Version="2.11.0" /> <PackageReference Include="Serilog.Extensions.Logging" Version="3.1.0" /> <PackageReference Include="Serilog.Settings.Configuration" Version="3.3.0" /> <PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" /> <PackageReference Include="Serilog.Sinks.File" Version="5.0.0" /> </ItemGroup> <ItemGroup> <None Update="appsettings.json"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </None> </ItemGroup> </Project>
Obviously, the below configuration classes would be specific to your application:
Configuration/Config.cs
namespace MyApplication.Configuration; public class Config { public Item Item { get; init; } = new(); public Widget[] Collection { get; init; } = Array.Empty<Widget>(); public string ApiPassword { get; init; } = ""; }
Configuration/Item.cs
namespace MyApplication.Configuration; public class Item { public int ID { get; init; } public string Name { get; init; } = ""; }
Configuration/Widget.cs
namespace MyApplication.Configuration; public class Widget { public string Color { get; init; } = ""; public string Shape { get; init; } = ""; }