Read or download the complete SDK project, then follow its setup on your computer. The separate C# companion practice below runs in SharpRune’s .NET 9 console environment.
Structured logging keeps values separate from the template
A log event can contain a stable template and named property values rather than one already interpolated string. That makes an accepted-count observation independently available to a provider that supports structured state. A scope can attach contextual fields to several related events. The external project uses the real JSON console provider and enables scope output; it does not install a telemetry collector. The portable companion merely computes three accepted positive amounts in batch seven. Those domain observations are useful inputs to instrumentation, but printing them does not pretend to exercise ILogger, log levels or provider formatting.
Find the batch event among ordinary hosting events
The console counts positive values in the array and prints accepted:3 and batch:7. On the server, /batch obtains a logger with category BatchLab, begins a scope containing BatchId seven and records Accepted {AcceptedCount} rows with the integer three. The response separately returns accepted three as JSON. The terminal also contains framework hosting events, so inspect the category and structured properties rather than selecting the first line that looks like a log. The verified batch event contains AcceptedCount in its state and BatchId in its scopes; no external log delivery is implied.
Own contextual fields for the period they describe
The scope is disposed before the handler exits, limiting its contextual association to that operation. This is different from placing a mutable global batch identifier beside a logger and assuming overlapping requests see the correct value. Scope support and rendering depend on provider configuration, which is why the example explicitly enables IncludeScopes on the JSON provider. The field is a bounded classroom identifier, not a copied request header or credential. When a scope carries actual request context, choose fields deliberately and ensure the lifetime follows the asynchronous operation that produces the events.
Avoid converting queryable state into an opaque string
Changing the log call to string interpolation can preserve a human-readable sentence while losing the named template value that a structured backend could query. Stable property names also help consumers compare events over time. That does not mean every value should become a dimension or be recorded without review: raw request bodies, tokens and unlimited user input have different privacy and volume consequences. The example's accepted count and fixed batch identifier keep the meaning narrow. Also distinguish a scope property from a message property; they can be displayed together without having identical ownership or naming responsibilities.
Practice: change data without changing the template
Change the accepted count to five while leaving the log template exactly the same. Predict the HTTP response, rendered message and structured AcceptedCount field, then inspect the real provider output. Next rename the template property deliberately and identify which downstream query would need an update even though the sentence still looks similar. Keep those changes separate. The hint is to inspect parsed JSON state, not just search terminal text for a number. A provider may render values attractively while still retaining or discarding different metadata depending on how the log call was constructed.
Practice: nest and end an operation scope
Add an inner scope with a fixed operation label and produce one event inside it and another after it ends but before the outer batch scope ends. Predict which contextual fields appear with each event. Then produce a third event after both scopes end and verify that neither contextual association leaks into unrelated work. Keep the logger category stable and use independent event names so the observations are easy to distinguish. Do not use a shared mutable global dictionary as a replacement scope mechanism; the exercise concerns the real provider's contextual lifetime behavior.
Treat logging as evidence with a configured destination
A successful handler result and a successful local console event are different observations. This project proves that the configured JSON console provider receives the intended named state and scope; it does not prove retention, indexing, export delivery or alerting. Production observability needs explicit provider and pipeline decisions, while application code should keep stable event meaning and clear ownership. Preserve a small provider-output check when changing templates or scopes, and keep domain tests independent of log formatting. That combination makes it possible to improve instrumentation without turning incidental console text into the only specification of the application's behavior.
- Use stable templates and intentional field names for observations clients need to query.
- Inspect scope lifetime and provider configuration separately from the HTTP result.
Structured logs and request scopes — real ASP.NET Core 10 project
Set up the SDK project
- Save all files into one new empty directory, preserving their relative paths. Install the official .NET 10 SDK if it is not already available.
- From that directory run: dotnet restore WebLab.csproj --source https://api.nuget.org/v3/index.json
- Start only on loopback: ASPNETCORE_ENVIRONMENT=Development dotnet run --project WebLab.csproj --no-launch-profile --urls http://127.0.0.1:5080
- Use another terminal for the requests below, then press Ctrl+C in this project’s terminal to stop its server.
- Call /batch and inspect the JSON console event with category BatchLab; look for AcceptedCount and the BatchId scope rather than matching only rendered text.
Download complete SDK project ↗
Project files
WebLab.csproj
Download file ↗<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
Program.cs
Download file ↗var builder = WebApplication.CreateBuilder(args); builder.Logging.ClearProviders(); builder.Logging.AddJsonConsole(options => options.IncludeScopes = true);
var app = builder.Build();
app.MapGet("/batch", (ILoggerFactory factory) => {
var logger = factory.CreateLogger("BatchLab"); using var scope = logger.BeginScope(new Dictionary<string, object> { ["BatchId"] = 7 });
logger.LogInformation("Accepted {AcceptedCount} rows", 3); return TypedResults.Ok(new { accepted = 3 });
});
app.Run();
Expected result
GET /batch returns accepted 3. The server JSON log contains category BatchLab, AcceptedCount=3 and the BatchId=7 scope; transport logs are separate events.
Use the listed SDK and dependencies for this project. Open C# practice runs the separate portable exercise.
C# companion practice
using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
var amounts = new[] { 2, -1, 3, 0, 4 }; int accepted = amounts.Count(x => x > 0); int batch = 7;
Console.WriteLine("accepted:" + accepted); Console.WriteLine("batch:" + batch);
C# practice output
accepted:3 batch:7
The message template stays stable; named values and a bounded scope remain separate data.