Services and Interfaces
The service layer only pays off if services are replaceable and testable β which is what interfaces and dependency injection buy you. This lesson is the plumbing that makes the architecture from lesson 1 actually work.
Interface-first services
Every service gets an interface (IPermitService) and controllers depend on that, injected
through the constructor:
public PermitsController(IPermitService permits)
{
_permits = permits;
}
The controller no longer knows or cares how permits are approved β it can be unit-tested with a
mock (Moq, NSubstitute), and the implementation can gain caching or logging without any
caller changing. Keep interfaces honest: they should describe operations
(ApprovePermit(id)) not leak persistence (GetQueryable()), and one interface per service is
plenty β resist the urge to build deep hierarchies.
DI registration and lifetimes
Someone has to construct the object graph. ASP.NET Core's built-in DI container does it β
you register in Program.cs:
builder.Services.AddScoped<IPermitService, PermitService>();
builder.Services.AddDbContext<AppDbContext>(o => o.UseSqlServer(cs));
The three lifetimes you must be able to recite:
- Transient β new instance every resolution. Safe default for stateless services.
- Scoped β one instance per HTTP request. Where
DbContextlives, and therefore where anything holding aDbContextshould live. - Singleton β one instance for the application lifetime. Only for immutable, thread-safe
things: configuration, caches,
IHttpClientFactory-managed handlers.
The classic bug is the captive dependency: a singleton service that takes a scoped
DbContext in its constructor captures the first request's context forever β stale data and
thread-safety crashes follow. Rule: a service's lifetime must be no longer than its
dependencies'. The built-in container validates this in the Development environment β treat any
scope-validation exception at startup as the bug it is, not as noise to suppress. Legacy note:
MVC 5-era apps wired third-party containers (Unity, Autofac) through DependencyResolver β
same concepts, but no captive-dependency validation, so review those registrations by hand.
Anti-patterns: service locators and static state
Two shortcuts sabotage the whole design. The service locator β
HttpContext.RequestServices.GetService<IPermitService>() scattered through the code β hides
dependencies from constructors, so nothing declares what it needs and tests can't substitute
anything without global setup. If a class needs a service, ask for it in the constructor,
period. Worse still is static mutable state: static DbContexts (not thread-safe β data
corruption under load), static caches without eviction or locking, static "current user"
holders that bleed across requests. Statics are invisible to the container, shared across all
requests, and un-mockable. When you inherit them β and in older agency codebases you will β
migrate the highest-traffic ones first, converting each into a registered singleton with an
interface so it at least becomes visible, testable, and thread-reviewed.