C# 6 min read

Dependency Injection in ASP.NET Core and Blazor

Dependency Injection is one of the most important patterns in modern .NET applications. This article explains how it works in ASP.NET Core and Blazor, including service lifetimes and common mistakes.

Admin
Admin
.NET & IoT Developer
Dependency Injection in ASP.NET Core and Blazor

Dependency Injection in ASP.NET Core and Blazor

Dependency Injection is one of the foundations of modern .NET development. If you build applications with ASP.NET Core or Blazor, you use it all the time, even if you do not always think about it directly.

Services, repositories, loggers, configuration, API clients, authentication providers and application services are usually registered in the DI container and injected where they are needed.

The main idea is simple: a class should not create all of its dependencies by itself. Instead, it should receive them from the outside.

This makes the code cleaner, easier to test and easier to maintain.

The problem Dependency Injection solves

Imagine a component that creates its own service directly:

private readonly BlogPostService _service = new BlogPostService();

This looks simple, but it creates problems.

The component is now tightly coupled to a concrete implementation. It is harder to test. It is harder to replace the service. It is harder to configure dependencies inside the service.

Dependency Injection solves this by moving object creation to the framework.

Instead of creating the service manually, you inject it:

@inject IBlogPostService BlogPostService

Now the component depends on an abstraction, not a concrete implementation.

Registering services

In ASP.NET Core and Blazor, services are usually registered in Program.cs.

Example:

builder.Services.AddScoped<IBlogPostService, BlogPostService>();

This tells the DI container:

When something asks for IBlogPostService, provide an instance of BlogPostService.

Then you can inject it into a component:

@inject IBlogPostService BlogPostService

Or into another service:

public class BlogDashboardService
{
    private readonly IBlogPostService _blogPostService;
public BlogDashboardService(IBlogPostService blogPostService)
{
    _blogPostService = blogPostService;
}

}

This pattern keeps your classes focused on their responsibilities.

Constructor injection

Constructor injection is the most common and recommended approach for services.

Example:

public class EmailNotificationService
{
    private readonly ILogger<EmailNotificationService> _logger;
    private readonly IConfiguration _configuration;
public EmailNotificationService(
    ILogger&lt;EmailNotificationService&gt; logger,
    IConfiguration configuration)
{
    _logger = logger;
    _configuration = configuration;
}

}

The dependencies are explicit. Anyone reading the constructor can see what the class needs.

This is better than hiding dependencies inside methods or creating them manually.

Injecting services in Blazor components

In Razor components, you can inject services with @inject.

@inject IBlogPostService BlogPostService

<h1>Latest Posts</h1>

@code { private IReadOnlyList<BlogPostDto>? posts;

protected override async Task OnInitializedAsync()
{
    posts = await BlogPostService.GetLatestPostsAsync();
}

}

This is clean and common.

However, avoid injecting too many services into one component. If a component needs many services, it may be doing too much.

In that case, create an application service that coordinates the work and inject only that service into the component.

Service lifetimes

ASP.NET Core has three main service lifetimes:

  • Transient
  • Scoped
  • Singleton

Choosing the right lifetime is important.

Transient services

A transient service is created every time it is requested.

builder.Services.AddTransient<IMyService, MyService>();

Use transient services for lightweight, stateless operations.

Good examples:

  • small helper services
  • formatters
  • mappers
  • simple validators

Avoid using transient services for expensive objects or state that must be shared.

Scoped services

A scoped service is created once per scope.

builder.Services.AddScoped<IUserSessionService, UserSessionService>();

In ASP.NET Core web applications, a scoped service usually lives for one HTTP request.

In Blazor Server, scoped services live for the current circuit, which is usually the connected user session.

This makes scoped services very useful for user-specific state in Blazor Server.

Examples:

  • current user state
  • selected dashboard filters
  • unit of work
  • application services
  • EF Core DbContext

In many Blazor applications, Scoped is the safest default.

Singleton services

A singleton service is created once and shared for the entire application lifetime.

builder.Services.AddSingleton<IClock, SystemClock>();

Use singleton services only when the service is stateless or intentionally shared globally.

Good examples:

  • configuration readers
  • application-wide cache
  • static lookup data
  • clock abstraction
  • background coordination services

Be careful with singleton services in Blazor Server. If you store user-specific data in a singleton, that data can be shared across users, which is usually a serious bug.

The most common lifetime mistake

The most common mistake is putting user-specific state into a singleton.

Example of a bad idea:

public class CurrentUserState
{
    public string? UserId { get; set; }
}

builder.Services.AddSingleton<CurrentUserState>();

In a multi-user application, this can mix state between users.

For user-specific state in Blazor Server, prefer scoped services:

builder.Services.AddScoped<CurrentUserState>();

This keeps the state tied to the current user circuit.

Injecting configuration

Configuration can also be injected.

public class ApiClient
{
    private readonly IConfiguration _configuration;
public ApiClient(IConfiguration configuration)
{
    _configuration = configuration;
}

public string BaseUrl =&gt; _configuration["Api:BaseUrl"]!;

}

For larger applications, strongly typed options are usually cleaner.

builder.Services.Configure<ApiOptions>(
    builder.Configuration.GetSection("Api"));

Then inject:

public class ApiClient
{
    private readonly ApiOptions _options;
public ApiClient(IOptions&lt;ApiOptions&gt; options)
{
    _options = options.Value;
}

}

This avoids scattering string keys throughout your code.

Dependency Injection and testing

One of the biggest benefits of Dependency Injection is testability.

If your class depends on an interface, you can replace the real implementation with a fake or mock during tests.

Example:

public class BlogPostService
{
    private readonly IDateTimeProvider _dateTimeProvider;
public BlogPostService(IDateTimeProvider dateTimeProvider)
{
    _dateTimeProvider = dateTimeProvider;
}

}

In production, you register:

builder.Services.AddSingleton<IDateTimeProvider, SystemDateTimeProvider>();

In tests, you can use:

var fakeClock = new FakeDateTimeProvider();

This makes the behavior predictable and testable.

Avoid service locator style

A common anti-pattern is injecting IServiceProvider everywhere and resolving services manually.

var service = serviceProvider.GetRequiredService<IMyService>();

Sometimes this is necessary in advanced scenarios, but it should not be your default approach.

Prefer constructor injection or @inject in components.

Explicit dependencies are easier to understand than hidden service resolution.

Keep dependencies pointing in the right direction

Dependency Injection works best when combined with clean architecture.

For example, the Application layer can define an interface:

public interface IEmailSender
{
    Task SendAsync(string to, string subject, string body);
}

The Infrastructure layer implements it:

public class SmtpEmailSender : IEmailSender
{
    public Task SendAsync(string to, string subject, string body)
    {
        // send email through SMTP
    }
}

The UI or application service depends on the interface, not the SMTP implementation.

This keeps technical details isolated.

Practical best practices

Use these rules in real projects:

  • depend on abstractions when it makes sense
  • use scoped services for user-specific Blazor state
  • avoid user data in singletons
  • keep services focused
  • avoid injecting too many services into one component
  • prefer constructor injection in classes
  • use @inject in Razor components
  • use strongly typed options for configuration
  • avoid manual service resolution unless necessary
  • keep business logic out of components

Final thoughts

Dependency Injection is not just a technical feature in ASP.NET Core. It is a design tool.

It helps you separate responsibilities, reduce coupling and keep your application testable.

In Blazor, it becomes even more important because components can easily become too powerful. Injecting clean application services allows your components to stay focused on rendering and user interaction.

Use the right service lifetime, keep dependencies explicit and avoid storing user-specific state in singletons.

That alone will prevent many architectural problems in real-world Blazor applications.

Share:

Become a member

Get the latest news right in your inbox. It's free and you can unsubscribe at any time. We hate spam as much as we do, so we never spam!

Read next