Resolving Services — GetRequiredService, Scopes and Service Locator

Constructor injection is the right choice 95% of the time. But some scenarios require explicitly resolving services from the container: background services (IHostedService instances are singletons that cannot inject scoped services), static helper methods, middleware that needs runtime-conditional services, and factory classes. IServiceProvider is the service resolution interface. IServiceScopeFactory creates isolated scopes for background work. Understanding when and how to resolve services explicitly — and the anti-patterns to avoid — rounds out the DI toolkit.

GetRequiredService vs GetService

// ── GetRequiredService — throws if not registered (preferred) ─────────────
var repo = serviceProvider.GetRequiredService<IPostRepository>();
// Throws InvalidOperationException if IPostRepository not registered
// Use this when the service MUST exist — a missing registration is a bug

// ── GetService — returns null if not registered ───────────────────────────
var repo2 = serviceProvider.GetService<IPostRepository>();
// Returns null if IPostRepository not registered — no exception
// Use only when the service is genuinely optional

// ── In middleware — IServiceProvider from HttpContext ─────────────────────
public class RequestLoggingMiddleware(RequestDelegate next)
{
    public async Task InvokeAsync(HttpContext context)
    {
        // Resolve scoped service from the request's scope (not root provider)
        var logger = context.RequestServices.GetRequiredService<IAuditLogger>();
        await logger.LogRequestAsync(context);
        await next(context);
    }
}
Note: context.RequestServices is the scoped IServiceProvider for the current HTTP request — it correctly resolves Scoped services. app.Services is the root IServiceProvider (Singleton scope) and will throw when you try to resolve Scoped services. In middleware, always use context.RequestServices to resolve per-request services. In action methods, always use constructor injection (the framework automatically uses the request scope for controller construction).
Tip: Enable DI validation on startup with ValidateOnBuild = true to catch missing registrations before the application serves its first request. Without this, a missing registration only surfaces as an exception on the first request that needs the missing service — potentially hours after deployment. With validation: builder.Host.UseDefaultServiceProvider(opts => { opts.ValidateScopes = true; opts.ValidateOnBuild = true; }). This is one of the highest-value defensive practices for ASP.NET Core applications.
Warning: The Service Locator anti-pattern is using IServiceProvider (or a static container) as a global registry that any class can call to retrieve dependencies. It hides dependencies (a class’s constructor no longer tells you what it needs), makes testing harder (you must configure the full container for every test), and creates invisible coupling. Use explicit constructor injection wherever possible. The legitimate uses of IServiceProvider are: background service scope creation, middleware, and factory methods that need runtime-conditional service resolution.

Creating Scopes in Background Services

// ── IServiceScopeFactory — create a scope for scoped service resolution ────
public class DataSyncService(
    IServiceScopeFactory scopeFactory,
    ILogger<DataSyncService> logger) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            await SyncDataAsync(stoppingToken);
            await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
        }
    }

    private async Task SyncDataAsync(CancellationToken ct)
    {
        // Create a scope for each unit of work — scoped services live for this scope
        using var scope = scopeFactory.CreateScope();
        var repo = scope.ServiceProvider.GetRequiredService<IPostRepository>();
        var sync = scope.ServiceProvider.GetRequiredService<ISyncService>();

        try
        {
            await sync.SyncAsync(ct);
            logger.LogInformation("Sync completed successfully.");
        }
        catch (Exception ex) when (!ct.IsCancellationRequested)
        {
            logger.LogError(ex, "Sync failed.");
        }
        // scope.Dispose() called here — DbContext and repo are disposed cleanly
    }
}

Startup DI Validation

// Program.cs — validate DI container on build (catches missing registrations)
builder.Host.UseDefaultServiceProvider((context, options) =>
{
    // ValidateOnBuild: checks all registrations are resolvable at startup
    options.ValidateOnBuild = true;
    // ValidateScopes: checks captive dependency issues (scoped in singleton)
    options.ValidateScopes  = context.HostingEnvironment.IsDevelopment();
});

Common Mistakes

Mistake 1 — Resolving scoped services from the root IServiceProvider

❌ Wrong — resolves from root scope (application lifetime), not request scope:

var repo = app.Services.GetRequiredService<IPostRepository>();  // throws for Scoped!

✅ Correct — create a scope: using var scope = app.Services.CreateScope(); var repo = scope.ServiceProvider.GetRequired...();

Mistake 2 — Using Service Locator as the primary resolution mechanism

❌ Wrong — passing IServiceProvider to every class that needs dependencies.

✅ Correct — constructor injection as the default; Service Locator only in the specific scenarios above.

🧠 Test Yourself

Why must a BackgroundService use IServiceScopeFactory to create a scope rather than directly injecting IPostRepository?