Blazor Component Created: 01 Feb 2026 Updated: 01 Feb 2026

Dependency Injection in Blazor: A Practical Guide with Real-World Examples

Dependency Injection (DI) is one of the most fundamental patterns in modern .NET development, and understanding it is crucial for building maintainable, testable, and scalable Blazor applications. This article explores DI in Blazor through practical, hands-on examples that demonstrate the three service lifetimes: Singleton, Scoped, and Transient.

By the end of this article, you'll have a deep understanding of when and how to use each service lifetime, backed by working code examples you can run and experiment with.

The Problem: Managing Dependencies and State

Before we dive into DI, let's understand the problem it solves. Imagine you're building a Blazor application with the following requirements:

  1. Weather data needs to be fetched fresh each time it's requested
  2. User session data (like shopping cart or preferences) should persist during the user's connection
  3. Application-wide audit logs should be shared across all users

Without DI, you might face these challenges:

  1. Tight coupling: Components directly instantiate their dependencies using new, making code hard to test and modify
  2. State management chaos: No clear pattern for deciding when objects should be shared or isolated
  3. Difficult testing: Can't easily mock dependencies for unit tests
  4. Hidden dependencies: Not clear what a component needs to function

Dependency Injection solves these problems by inverting the control of object creation. Instead of components creating their dependencies, they declare what they need, and the DI container provides it.

Understanding DI in Blazor

DI is a technique to implement Inversion of Control (IoC), where dependencies are "injected" into classes rather than being instantiated within them. In Blazor, we configure DI in the Program.cs file and consume services in components or other services.

The Three Service Lifetimes

Blazor supports three service lifetimes:

  1. Singleton: One instance for the entire application lifetime, shared by all users
  2. Scoped: One instance per user connection (circuit in Blazor Server) or per browser session (in WebAssembly)
  3. Transient: A new instance every time the service is requested

Choosing the right lifetime is critical for proper behavior and avoiding bugs.

Setting Up the Services

Let's create practical services to demonstrate each lifetime. We'll build a mini-application with weather forecasts, user state management, audit logging, and notifications.

Service Interfaces and Implementations

First, we define our services with a ServiceId property to track instances:

1. Weather Forecast Service (Transient)

This service provides weather data and should be stateless:

namespace BlazorApp.Services;

public interface IWeatherForecastService
{
Task<IEnumerable<WeatherForecast>> GetForecastsAsync();
Guid ServiceId { get; }
}

public class WeatherForecastService : IWeatherForecastService
{
public Guid ServiceId { get; } = Guid.NewGuid();

public Task<IEnumerable<WeatherForecast>> GetForecastsAsync()
{
var startDate = DateOnly.FromDateTime(DateTime.Now);
var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild",
"Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
var forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = startDate.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = summaries[Random.Shared.Next(summaries.Length)]
});

return Task.FromResult(forecasts);
}
}

public class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public string? Summary { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}

Why Transient? Weather data is stateless and doesn't need to maintain state between requests. Each component or operation can get a fresh instance.

2. User State Service (Scoped)

This service tracks user-specific data:

namespace BlazorApp.Services;

public interface IUserStateService
{
string? UserName { get; set; }
int PageViews { get; }
void IncrementPageViews();
Guid ServiceId { get; }
}

public class UserStateService : IUserStateService
{
public Guid ServiceId { get; } = Guid.NewGuid();
public string? UserName { get; set; }
public int PageViews { get; private set; }

public void IncrementPageViews()
{
PageViews++;
}
}

Why Scoped? User state should be isolated per user connection. Each browser tab (in Blazor Server) gets its own instance, preventing state from leaking between users.

3. Audit Service (Singleton)

This service maintains application-wide audit logs:

namespace BlazorApp.Services;

public interface IAuditService
{
void LogAction(string action);
List<string> GetAuditLog();
Guid ServiceId { get; }
}

public class AuditService : IAuditService
{
private readonly List<string> _auditLog = new();
public Guid ServiceId { get; } = Guid.NewGuid();

public void LogAction(string action)
{
_auditLog.Add($"{DateTime.Now:HH:mm:ss} - {action}");
}

public List<string> GetAuditLog()
{
return new List<string>(_auditLog);
}
}

Why Singleton? Audit logs should be centralized and shared across all users for monitoring and debugging purposes.

4. Counter Service (Scoped)

A simple counter to demonstrate scoped behavior:

namespace BlazorApp.Services;

public interface ICounterService
{
int Count { get; }
void Increment();
Guid ServiceId { get; }
}

public class CounterService : ICounterService
{
public Guid ServiceId { get; } = Guid.NewGuid();
public int Count { get; private set; }

public void Increment()
{
Count++;
}
}

Why Scoped? Counters should maintain state during a user's session but reset for new connections.

Registering Services in Program.cs

Now we configure our services in both the Server and Client projects:

Server Project (BlazorApp/Program.cs)

using BlazorApp.Services;

var builder = WebApplication.CreateBuilder(args);

// Singleton: Shared across all users and the entire application lifetime
builder.Services.AddSingleton<IAuditService, AuditService>();

// Scoped: New instance per user connection (or per-request in SSR)
builder.Services.AddScoped<IUserStateService, UserStateService>();
builder.Services.AddScoped<ICounterService, CounterService>();

// Transient: New instance every time it's requested
builder.Services.AddTransient<IWeatherForecastService, WeatherForecastService>();
builder.Services.AddTransient<INotificationService, NotificationService>();

builder.Services.AddRazorComponents()
.AddInteractiveServerComponents()
.AddInteractiveWebAssemblyComponents();

// ... rest of configuration

Client Project (BlazorApp.Client/Program.cs)

using BlazorApp.Services;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;

var builder = WebAssemblyHostBuilder.CreateDefault(args);

// In WebAssembly, Scoped behaves like Singleton since everything runs in the browser
// But we still use Scoped for services that conceptually belong to a user session
builder.Services.AddScoped<IUserStateService, UserStateService>();
builder.Services.AddScoped<ICounterService, CounterService>();

// Transient services are still created fresh each time
builder.Services.AddTransient<IWeatherForecastService, WeatherForecastService>();
builder.Services.AddTransient<INotificationService, NotificationService>();

// Singleton for truly application-wide services
builder.Services.AddSingleton<IAuditService, AuditService>();

await builder.Build().RunAsync();

Important Note for WebAssembly: In Blazor WebAssembly, Scoped services behave like Singleton because there's no server-side connection. However, we still use Scoped for semantic clarity—it indicates the service is conceptually user-scoped.

Consuming Services in Components

There are three ways to inject services into components:

1. Using @inject Directive (Most Common)

@page "/scoped-demo"
@inject IUserStateService UserState
@inject ICounterService Counter

<h1>Scoped Services Demo</h1>
<p>User: @UserState.UserName</p>
<p>Page Views: @UserState.PageViews</p>
<button @onclick="UserState.IncrementPageViews">Increment</button>

2. Using [Inject] Attribute (Code-Behind)

public partial class ScopedDemo
{
[Inject]
public IUserStateService UserState { get; set; } = default!;
[Inject]
public ICounterService Counter { get; set; } = default!;
}

3. Constructor Injection (Service-to-Service)

When a service depends on other services, use constructor injection:

namespace BlazorApp.Services;

public class OrderService : IOrderService
{
private readonly INotificationService _notificationService;
private readonly IAuditService _auditService;

public OrderService(
INotificationService notificationService,
IAuditService auditService)
{
_notificationService = notificationService;
_auditService = auditService;
}

public void PlaceOrder(string item, int quantity)
{
var orderMessage = $"Order placed: {quantity}x {item}";
_auditService.LogAction(orderMessage);
_notificationService.SendNotification(orderMessage);
}
}

Why Constructor Injection for Services? It makes dependencies explicit and enables compile-time checks. The @inject directive is only available in Blazor components.

Practical Examples: Interactive Demos

Let's build interactive pages that demonstrate each lifetime in action.

Example 1: Scoped Services Demo

This page shows how scoped services maintain state per user connection:

@page "/di-scoped-demo"
@rendermode InteractiveServer
@inject IUserStateService UserState
@inject ICounterService Counter

<PageTitle>Scoped Services Demo</PageTitle>

<h1>Scoped Services Demo</h1>

<div class="alert alert-info">
<strong>Scoped Lifetime:</strong> Each user gets their own instance.
Open this page in multiple tabs to see independent state.
</div>

<div class="card mb-3">
<div class="card-header">
<h3>User State Service</h3>
<small class="text-muted">Service ID: @UserState.ServiceId</small>
</div>
<div class="card-body">
<label>User Name:</label>
<input type="text" @bind="UserState.UserName" class="form-control" />
<p>Page Views: <strong>@UserState.PageViews</strong></p>
<button class="btn btn-primary" @onclick="UserState.IncrementPageViews">
Increment Page Views
</button>
</div>
</div>

<div class="card">
<div class="card-header">
<h3>Counter Service</h3>
<small class="text-muted">Service ID: @Counter.ServiceId</small>
</div>
<div class="card-body">
<p>Current count: <strong>@Counter.Count</strong></p>
<button class="btn btn-success" @onclick="Counter.Increment">Increment</button>
</div>
</div>

@code {
protected override void OnInitialized()
{
UserState.IncrementPageViews();
}
}

Try this:

  1. Set your username and increment counters
  2. Open the page in a new browser tab
  3. Notice the Service IDs are different and counters start at zero
  4. Each tab maintains independent state

Example 2: Singleton Services Demo

This demonstrates how singleton services share state across all users:

@page "/di-singleton-demo"
@rendermode InteractiveServer
@inject IAuditService Audit

<PageTitle>Singleton Services Demo</PageTitle>

<h1>Singleton Services Demo</h1>

<div class="alert alert-warning">
<strong>Singleton Lifetime:</strong> One instance shared across ALL users.
Changes made by one user are visible to all others.
</div>

<div class="card">
<div class="card-header">
<h3>Audit Service (Singleton)</h3>
<small class="text-muted">Service ID: @Audit.ServiceId</small>
</div>
<div class="card-body">
<input type="text" @bind="newAction" placeholder="Enter action" class="form-control" />
<button class="btn btn-primary mt-2" @onclick="LogAction">Log Action</button>
<h4 class="mt-3">Audit Log (Shared)</h4>
<ul class="list-group">
@foreach (var log in auditLog)
{
<li class="list-group-item">@log</li>
}
</ul>
</div>
</div>

@code {
private string newAction = string.Empty;
private List<string> auditLog = new();

protected override void OnInitialized()
{
auditLog = Audit.GetAuditLog();
}

private void LogAction()
{
if (!string.IsNullOrWhiteSpace(newAction))
{
Audit.LogAction(newAction);
newAction = string.Empty;
auditLog = Audit.GetAuditLog();
}
}
}

Try this:

  1. Log some actions
  2. Open the page in a new tab or incognito window
  3. Notice the Service ID is the same across all tabs
  4. Actions logged in one tab appear in all tabs

WARNING: Never store user-specific data in singleton services! This would leak data between users.

Example 3: Transient Services Demo

This shows how transient services create new instances for each injection:

@page "/di-transient-demo"
@rendermode InteractiveServer
@inject IWeatherForecastService Weather1
@inject IWeatherForecastService Weather2
@inject INotificationService Notification

<PageTitle>Transient Services Demo</PageTitle>

<h1>Transient Services Demo</h1>

<div class="alert alert-success">
<strong>Transient Lifetime:</strong> New instance every time it's requested.
Even within the same component, each injection gets its own instance.
</div>

<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h3>Weather Service #1</h3>
<small class="text-muted">Service ID: @Weather1.ServiceId</small>
</div>
<div class="card-body">
<button class="btn btn-primary" @onclick="LoadWeather1">Load Forecast</button>
@if (forecasts1 != null)
{
<ul class="mt-3">
@foreach (var forecast in forecasts1)
{
<li>@forecast.Date: @forecast.TemperatureC°C</li>
}
</ul>
}
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h3>Weather Service #2</h3>
<small class="text-muted">Service ID: @Weather2.ServiceId</small>
</div>
<div class="card-body">
<button class="btn btn-primary" @onclick="LoadWeather2">Load Forecast</button>
@if (forecasts2 != null)
{
<ul class="mt-3">
@foreach (var forecast in forecasts2)
{
<li>@forecast.Date: @forecast.TemperatureC°C</li>
}
</ul>
}
</div>
</div>
</div>
</div>

@code {
private IEnumerable<WeatherForecast>? forecasts1;
private IEnumerable<WeatherForecast>? forecasts2;

private async Task LoadWeather1()
{
forecasts1 = await Weather1.GetForecastsAsync();
}

private async Task LoadWeather2()
{
forecasts2 = await Weather2.GetForecastsAsync();
}
}

Key Observation: Both Weather1 and Weather2 have different Service IDs despite being injected into the same component. This proves they're separate instances.

Example 4: Service-to-Service Dependency Injection

This demonstrates how services can depend on other services:

@page "/di-constructor-injection"
@rendermode InteractiveServer
@inject IOrderService OrderService
@inject IAuditService AuditService

<h1>Constructor Injection Demo</h1>

<div class="alert alert-primary">
<strong>Pattern:</strong> OrderService depends on NotificationService and AuditService.
The DI container automatically resolves the entire dependency chain.
</div>

<div class="card">
<div class="card-body">
<label>Item Name:</label>
<input type="text" @bind="itemName" class="form-control" />
<label>Quantity:</label>
<input type="number" @bind="quantity" class="form-control" />
<button class="btn btn-primary mt-2" @onclick="PlaceOrder">Place Order</button>
</div>
</div>

<div class="card mt-3">
<div class="card-header">
<h3>Audit Log</h3>
</div>
<div class="card-body">
<ul>
@foreach (var log in auditLog)
{
<li>@log</li>
}
</ul>
</div>
</div>

@code {
private string itemName = string.Empty;
private int quantity = 1;
private List<string> auditLog = new();

private void PlaceOrder()
{
if (!string.IsNullOrWhiteSpace(itemName))
{
OrderService.PlaceOrder(itemName, quantity);
itemName = string.Empty;
quantity = 1;
auditLog = AuditService.GetAuditLog();
}
}
}

How it works:

  1. Component injects IOrderService
  2. OrderService constructor requests INotificationService and IAuditService
  3. DI container automatically creates and injects these dependencies
  4. When you place an order, both audit logging and notification happen automatically

Best Practices and Common Pitfalls

When to Use Each Lifetime

Lifetime - Use Cases - Examples
SingletonApplication-wide data, stateless utilities, cachingConfiguration, logging aggregators, application cache
ScopedUser-specific data, per-request stateAuthentication state, shopping cart, user preferences
TransientStateless operations, lightweight servicesAPI clients, data validators, mappers

Critical Rules

  1. Avoid Captive Dependencies
  2. ❌ Singleton should NOT depend on Scoped services
  3. ✅ Scoped can depend on Singleton
  4. ✅ Transient can depend on both
// BAD: Singleton depends on Scoped
public class MySingletonService
{
// ❌ Don't do this!
public MySingletonService(IScopedService scoped) { }
}

// GOOD: Scoped depends on Singleton
public class MyScopedService
{
// ✅ This is fine
public MyScopedService(ISingletonService singleton) { }
}
  1. Thread Safety in Singletons
  2. Singleton services must be thread-safe since they're shared
  3. Use locks or concurrent collections when modifying state
using System.Collections.Concurrent;

public class AuditService : IAuditService
{
private readonly ConcurrentBag<string> _auditLog = new();

public void LogAction(string action)
{
_auditLog.Add($"{DateTime.Now:HH:mm:ss} - {action}");
}
}
  1. Never Store User Data in Singletons
  2. Leads to data leaks between users
  3. Security and privacy nightmare
  4. Use Scoped services for user-specific data
  5. Prefer Interfaces Over Concrete Types
  6. Enables testing with mocks
  7. Allows swapping implementations
  8. Follows Dependency Inversion Principle
// GOOD
builder.Services.AddScoped<IUserStateService, UserStateService>();

// LESS FLEXIBLE (but okay if only one implementation exists)
builder.Services.AddScoped<UserStateService>();
  1. WebAssembly Considerations
  2. Scoped = Singleton in WebAssembly (no server connection)
  3. Still use Scoped for semantic clarity
  4. Makes code portable between Server and WebAssembly

Common Mistakes

Mistake 1: Creating services with new

// ❌ Bad: Bypasses DI
var service = new UserStateService();

// ✅ Good: Let DI provide it
@inject IUserStateService UserState

Mistake 2: Mixing lifetimes incorrectly

// ❌ Bad: Singleton depends on Scoped
builder.Services.AddSingleton<GlobalService>(sp =>
new GlobalService(sp.GetRequiredService<IScopedService>()));

// ✅ Good: Use factory or IServiceProvider when needed
builder.Services.AddSingleton<GlobalService>();

Mistake 3: Forgetting to register services

// ❌ Forgot to register
@inject IMyService MyService // Runtime error!

// ✅ Register in Program.cs
builder.Services.AddScoped<IMyService, MyService>();

Testing with Dependency Injection

DI makes testing much easier. Here's an example using xUnit and NSubstitute:

using NSubstitute;
using Xunit;

public class OrderServiceTests
{
[Fact]
public void PlaceOrder_LogsAction()
{
// Arrange
var mockNotification = Substitute.For<INotificationService>();
var mockAudit = Substitute.For<IAuditService>();
var orderService = new OrderService(mockNotification, mockAudit);
// Act
orderService.PlaceOrder("Laptop", 2);
// Assert
mockAudit.Received(1).LogAction(
Arg.Is<string>(s => s.Contains("Laptop") && s.Contains("2"))
);
mockNotification.Received(1).SendNotification(Arg.Any<string>());
}
[Fact]
public void PlaceOrder_SendsNotification()
{
// Arrange
var mockNotification = Substitute.For<INotificationService>();
var mockAudit = Substitute.For<IAuditService>();
var orderService = new OrderService(mockNotification, mockAudit);
// Act
orderService.PlaceOrder("Mouse", 5);
// Assert
mockNotification.Received(1).SendNotification(
Arg.Is<string>(s => s.Contains("Mouse") && s.Contains("5"))
);
}
}

Benefits:

  1. Easy to mock dependencies
  2. Test in isolation
  3. Verify interactions
  4. No need for real implementations
  5. Fast test execution

Blazor Server vs WebAssembly Differences

Blazor Server

  1. Scoped = per SignalR connection (circuit)
  2. State maintained on server
  3. Connection loss = state loss
  4. Multiple tabs = multiple circuits = separate state

Blazor WebAssembly

  1. Scoped = effectively Singleton (runs in browser)
  2. State maintained client-side
  3. No server connection
  4. Refresh = state loss (unless persisted to localStorage)

Hybrid Apps (.NET 8+)

With @rendermode InteractiveAuto:

  1. Starts as Server, then downloads WebAssembly
  2. Services must be registered in both projects
  3. Scoped behavior may change during lifecycle

Advanced: Runtime Service Resolution

Sometimes you need to resolve services dynamically:

@inject IServiceProvider ServiceProvider

@code {
private void ResolveAtRuntime()
{
// Resolve a service at runtime
var service = ServiceProvider.GetRequiredService<IMyService>();
// Create a scope manually
using var scope = ServiceProvider.CreateScope();
var scopedService = scope.ServiceProvider
.GetRequiredService<IMyScopedService>();
// Use the service
scopedService.DoSomething();
}
private void ResolveOptionalService()
{
// Safely resolve optional services
var optionalService = ServiceProvider.GetService<IOptionalService>();
if (optionalService != null)
{
optionalService.DoSomething();
}
}
}

Use sparingly: Prefer constructor/property injection when possible. Runtime resolution hides dependencies and makes code less maintainable.

Share this lesson: