Exceptions, Logging, and DI
These three topics decide how maintainable an MVC codebase is five years in. They're also where
line-of-business apps accumulate the worst habits β swallowed exceptions, string.Format logging,
and controllers that new up their own dependencies.
An exception strategy you can defend
The rule: catch an exception only where you can do something about it. For most code that
means don't catch β let it bubble up to the app's exception-handling middleware
(UseExceptionHandler("/Error") in Program.cs), which logs it and shows a friendly error page.
Legitimate local catches are the boundary cases: retrying a transient SQL error, translating
a third-party exception into a domain one (throw new OrderNotFoundException(id, ex) β keep the
inner exception), or a top-level catch in a background job.
The anti-patterns to hunt in review: catch (Exception) { } (swallowing β the bug still
happens, now invisibly), catch { return null; } (moves the crash somewhere confusing), and
throw ex; instead of throw; (rethrowing with ex resets the stack trace and destroys the
evidence). Use exceptions for exceptional states, not flow control β "user not found" on a
lookup screen is a normal result, not a throw.
Logging that answers questions
The test of a log entry: can you diagnose a production incident from it without reproducing the bug? That requires context, and context is why structured logging won. Instead of baking values into a string, you log named properties:
_logger.LogWarning("Payment declined for {OrderId} amount {Amount}", orderId, amount);
The values are stored as fields, so you can query "all events where OrderId = 4412" across
services. ILogger<T> is built in β inject it anywhere; teams typically plug Serilog in
as the sink for files, Seq, or Application Insights. Practical levels: Error = a request
failed, Warning = something's off but handled, Information = business milestones,
Debug = noise you enable temporarily (per-namespace, in appsettings.json). Never log
passwords, connection strings, or personal data β in a public-sector agency, logs are
discoverable records.
Dependency injection: the shape of the whole app
Constructor injection is the pattern underneath everything else in this course: a controller
declares what it needs (public OrdersController(IOrderService orders) β a natural fit for C# 12
primary constructors) and the container supplies it. Benefits: dependencies are visible
in the signature, and tests can pass fakes.
The container is built in β you register services in Program.cs
(builder.Services.AddScoped<IOrderService, OrderService>()). The vocabulary that matters is
lifetimes: transient (new instance every resolve), scoped (one per HTTP request β the
right home for an EF DbContext), and singleton (one for the app; must be thread-safe and
must never capture a scoped service). The classic lifetime bug is a singleton holding a DbContext:
it works on your machine and corrupts state under concurrent load. When in doubt, register scoped.