Dependency Injection, Configuration, and Logging
Compose .NET applications through explicit dependencies, validate typed configuration, and produce structured logs without service-location or secret exposure.
Before this lesson
Choose DI lifetimes from ownership
Validate configuration at startup
Write structured operational logs
The short answer
Constructor injection makes required collaborators visible and testable. Register lifetimes from ownership semantics, bind and validate typed options at startup, and log structured events with stable property names rather than concatenated prose.
Build the runtime mental model
The built-in container constructs an object graph from registrations. Transient creates per resolution, scoped shares within a scope such as a request, and singleton lives for the application. A longer-lived service must not capture a shorter-lived dependency.
Advanced C# work improves when you separate language syntax, runtime behavior, and application policy. Write down which layer owns the guarantee in this lesson. Then identify the observable evidence—a compiler rejection, test result, generated query, trace, or measurement—that would prove the model correct.
Design the boundary deliberately
Use options classes for related configuration and validate required values, ranges, and relationships during startup. Logging templates preserve named fields for filtering and aggregation. Correlation identifiers connect events across a request.
The starter isolates one part of the mental model so it can run in the browser. The exercise moves the same rule into a current local .NET project where packages, framework hosting, diagnostics, and multi-file tests are available.
using System;
interface IClock { DateTime UtcNow { get; } }
class SystemClock : IClock { public DateTime UtcNow { get { return DateTime.UtcNow; } } }
class ReportService
{
private readonly IClock clock;
public ReportService(IClock clock) { this.clock = clock; }
public int CurrentYear() { return clock.UtcNow.Year; }
}
class Program
{
static void Main() { Console.WriteLine(new ReportService(new SystemClock()).CurrentYear() > 2020); }
}Expected output
True
Diagnose failure and misuse
Injecting IServiceProvider and resolving arbitrary services hides dependencies. Singleton mutable state creates contention and cross-request leaks. Logging tokens, connection strings, personal data, or entire request bodies creates security and compliance risk.
Classify each failure as a contract violation, transient operational failure, permanent dependency response, concurrency conflict, or programmer defect. That classification determines whether to reject, retry, compensate, cancel, or fail fast. A generic catch-and-continue policy destroys the information needed to make that decision.
| Question | Evidence to inspect | Decision |
|---|---|---|
| Is the input valid? | Validation result and boundary examples | Reject with a stable contract |
| Is the failure transient? | Typed status, exception, and policy context | Retry only when bounded and safe |
| Is state still consistent? | Invariant and transaction outcome | Commit, compensate, or abort |
| Is performance acceptable? | Representative latency and allocation data | Keep simple or optimize one cause |
Apply the concept in production
Keep the composition root near startup, fail fast on invalid configuration, document environment overrides, and test registrations by building the provider and resolving important roots. Define log levels and redaction rules.
Finish by making the result operable. Add structured diagnostics at the boundary, propagate cancellation, avoid sensitive data, and record SDK and dependency versions. Test the public behavior instead of private implementation details. If a framework or provider performs translation, serialization, concurrency, or I/O, include at least one test against the real production technology.
A senior-level review should be able to answer four questions: what contract is promised, who owns lifetime and cleanup, how failures become visible, and what evidence supports the design. If any answer depends on “the framework probably handles it,” inspect the documentation or runtime behavior and turn the assumption into a checked decision.
Quick knowledge check
Answer before you reveal.
01What is a captive dependency?
A longer-lived service captures a shorter-lived service, extending its lifetime and often causing unsafe shared state or disposal problems.
02What must happen before adding complexity to this design?
State the requirement, preserve a correct baseline, collect evidence, and explain how the proposed mechanism improves a specific quality.
Exercise
Practice challenge
Compose a service with scoped repository, singleton clock, validated options, and structured logging; include a test that detects a captive dependency.
Requirements
- The implementation states its contract and ownership boundary explicitly
- Automated checks cover the successful path and at least two meaningful failures
- Diagnostics expose failure context without secrets or swallowed exceptions
- The project documents required SDK, packages, setup, run, and test commands
Optional extension: Measure or load-test the critical path and record whether the evidence justifies another optimization or abstraction.
Open in C# compilerLesson checkpoint
One small step locks it in
Mark this lesson complete, then keep the momentum going.
Clear up the details
Frequently asked questions
When should I use dependency injection, configuration, and logging?
Use it when its explicit tradeoff solves a measured requirement or clarifies an owned boundary. Keep the simpler design when the additional mechanism does not improve correctness, operability, or changeability.
Does the browser compiler cover the complete production setup?
No. It runs the focused starter program. Framework, package, database, benchmark, and multi-project work requires a current local .NET SDK and the project commands described in the exercise.
What evidence should I keep after the exercise?
Keep the acceptance cases, automated tests, diagnostic or benchmark output where relevant, and a short decision note describing the chosen boundary and rejected alternative.