Configuration Files and Logging¶
ConsoleMVC reads settings and writes logs with the standard .NET libraries (Microsoft.Extensions.Configuration,
Microsoft.Extensions.Options and Microsoft.Extensions.Logging), the same ones ASP.NET Core uses.
Configuration¶
builder.Configuration is filled when the builder is created, from these sources. Later ones win:
| Source | Example |
|---|---|
appsettings.json in the application's folder (optional) |
{ "Shop": { "Name": "Corner" } } |
appsettings.{Environment}.json (optional) |
appsettings.Development.json |
Environment variables (__ separates sections) |
Shop__Name=Corner |
| Command-line arguments | dotnet run -- --Shop:Name=Corner |
The template's project copies appsettings*.json next to the application when it builds:
<ItemGroup>
<None Update="appsettings*.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
Read values directly, or bind a section to a class and inject it:
var builder = MvcApplication.CreateBuilder(args);
var pageSize = builder.Configuration.GetValue("Shop:PageSize", 20);
builder.Services.Configure<ShopOptions>(builder.Configuration.GetSection("Shop"));
builder.Configuration.AddJsonFile("local.json", optional: true); // add your own sources
public sealed class ShopOptions
{
public string Name { get; set; } = "";
public int PageSize { get; set; } = 20;
}
public class OrdersController(IOptions<ShopOptions> shop) : Controller
{
public ActionResult Index() => View(new OrdersModel { Title = shop.Value.Name });
}
Views can inject configuration too:
@inject Microsoft.Extensions.Configuration.IConfiguration Config
<text>@Config["Shop:Name"]</text>
IConfiguration is registered as a service and is also app.Configuration.
Checking settings at start-up¶
Options validated with ValidateOnStart() are checked by Build(), so a bad setting fails before the first screen:
builder.Services.AddOptions<ShopOptions>()
.Bind(builder.Configuration.GetSection("Shop"))
.Validate(o => o.PageSize is > 0 and <= 100, "Shop:PageSize must be between 1 and 100.")
.ValidateOnStart();
Secrets
Keep passwords and API keys out of appsettings.json, which is usually committed: use environment variables or a
secret store.
Environment¶
builder.Environment (and app.Environment, or an injected IHostEnvironment) tells where the application runs:
if (builder.Environment.IsDevelopment())
builder.Logging.SetMinimumLevel(LogLevel.Debug);
| Property | Value |
|---|---|
EnvironmentName |
DOTNET_ENVIRONMENT, or Production when it is not set |
ContentRootPath |
where configuration files are read from: the application's folder |
ApplicationName |
the entry assembly's name |
Choose them in code with MvcApplicationOptions, for example in tests:
var builder = MvcApplication.CreateBuilder(new MvcApplicationOptions
{
Args = args,
EnvironmentName = "Development",
ContentRootPath = testDirectory
});
Logging¶
Inject ILogger<T> into controllers, services and views
(@inject Microsoft.Extensions.Logging.ILogger<MyApp.Views.Orders.IndexView> Log):
public class OrdersController(ILogger<OrdersController> logger) : Controller
{
public ActionResult Cancel(int id)
{
logger.LogInformation("Cancelling order {OrderId}", id);
…
}
}
No log provider is added by default: the console shows your application, and a console logger would write over it. Write logs to a file instead, with the built-in file logger:
builder.Logging.AddFile("logs/app.log"); // relative to the application's folder
2026-10-27T21:04:05.123Z INFO ConsoleMVC.Mvc.MvcApplication: Starting at Home/Index (Production)
2026-10-27T21:04:09.871Z WARN Shop.Controllers.OrdersController: Order 42 has no address
Entries are written on a background task, so logging never makes a screen wait for the disk. The file rolls over by
size (app.log → app.1.log → app.2.log), continuation lines (exceptions, line breaks in a message) are indented
so a logged value cannot forge an entry, and the file can be followed with tail -f. The settings can also come from
the configuration, which AddFile() reads from Logging:File:
{
"Logging": {
"File": {
"Path": "logs/shop.log",
"MaxFileSizeBytes": 1048576,
"RetainedFileCount": 3,
"LogLevel": { "Default": "Debug" }
}
}
}
| Option | Default | Purpose |
|---|---|---|
Path |
logs/app.log |
The file, relative to the content root |
MaxFileSizeBytes |
10 MB | Roll over at this size; 0 never rolls over |
RetainedFileCount |
5 | Rolled-over files kept besides the current one |
QueueLength |
10 000 | Entries waiting to be written; when the disk cannot keep up, newer ones are dropped and counted |
Any other provider works too, for example builder.Logging.AddDebug() (Microsoft.Extensions.Logging.Debug) for the
debugger's output window.
Levels follow the Logging section, as in ASP.NET Core:
{
"Logging": {
"LogLevel": { "Default": "Information", "ConsoleMVC": "Debug" }
}
}
The framework logs under the ConsoleMVC.Mvc.MvcApplication category:
| Level | Message |
|---|---|
| Information | Starting at Home/Index (Production) and Stopped |
| Debug | Running Orders/Details for every screen or redirect |
| Error | Unhandled exception on Orders/Save, with the exception (it is still thrown to the caller) |
A remote sessions server also logs under ConsoleMVC.Mvc.TerminalServer:
| Level | Message |
|---|---|
| Information | Serving remote sessions on 127.0.0.1:2323 (Telnet, no TLS) and Stopped serving remote sessions |
| Information | Session 3f2a… started for 127.0.0.1:50122 (XTERM-256COLOR, 100x30) and Session 3f2a… ended |
| Warning | Connection from … refused: the session limit is reached (also: the filter, a failed TLS handshake) |
| Error | Session 3f2a… failed, and Connection from … failed while it was set up, with the exception; other sessions carry on |