DI Service – Tổng hợp các cách dùng trong Blazor

Trang này tổng hợp các cách dependency injection (DI) thường gặp trong Blazor: inject vào component, inject vào service khác, đăng ký bằng factory, inject built-in service, state service có event, và dùng interface với DI.


1️⃣ Inject service bằng @inject trong Razor

Đây là cách phổ biến nhất khi dùng service trong component hoặc page.

Xem code
Service (Services/CounterService.cs)

namespace BlazorSample.Services
{
    public class CounterService
    {
        public int Count { get; private set; }

        public void Increment()
        {
            Count++;
        }
    }
}
Razor (Pages/CounterPage.razor)
@page "/counter-page"
@inject BlazorSample.Services.CounterService CounterService

<h3>Counter Page</h3>

<p>Count: @CounterService.Count</p>

<button class="btn btn-primary" @onclick="CounterService.Increment">
    Increment
</button>
Program.cs

builder.Services.AddScoped<CounterService>();

2️⃣ Inject bằng thuộc tính với [Inject]

Dùng attribute [Inject] để inject service vào property trong phần @code. Cách này tương đương @inject nhưng viết theo kiểu C#.

Xem code
Service (Services/TimeService.cs)

namespace BlazorSample.Services
{
    public class TimeService
    {
        public string GetCurrentTime()
        {
            return DateTime.Now.ToString("HH:mm:ss");
        }
    }
}
Razor (Pages/InjectPropertyDemo.razor)
@page "/inject-property-demo"

<h3>Inject bằng [Inject]</h3>

<p>Current Time: @TimeService.GetCurrentTime()</p>

<button class="btn btn-primary" @onclick="Refresh">
    Refresh
</button>

@code {
    [Inject]
    public TimeService TimeService { get; set; } = default!;

    private void Refresh()
    {
        StateHasChanged();
    }
}
Program.cs

builder.Services.AddScoped<TimeService>();

3️⃣ Constructor injection trong service

Component thường dùng @inject, còn service thường dùng constructor injection.

Xem code
LoggerService (Services/LoggerService.cs)

namespace BlazorSample.Services
{
    public class LoggerService
    {
        public void Log(string message)
        {
            Console.WriteLine(message);
        }
    }
}
CounterService (Services/CounterService.cs)

namespace BlazorSample.Services
{
    public class CounterService
    {
        private readonly LoggerService _logger;

        public CounterService(LoggerService logger)
        {
            _logger = logger;
        }

        public int Count { get; private set; }

        public void Increment()
        {
            Count++;
            _logger.Log($"Count = {Count}");
        }
    }
}
Razor (Pages/CounterWithLogger.razor)
@page "/counter-with-logger"
@inject BlazorSample.Services.CounterService CounterService

<h3>Counter with Logger</h3>

<p>Count: @CounterService.Count</p>

<button class="btn btn-primary" @onclick="CounterService.Increment">
    Increment
</button>
Program.cs

builder.Services.AddScoped<LoggerService>();
builder.Services.AddScoped<CounterService>();

4️⃣ Một service phụ thuộc nhiều service khác

DI container sẽ tự resolve toàn bộ dependency tree nếu các service đã được đăng ký.

Xem code
UserService (Services/UserService.cs)

namespace BlazorSample.Services
{
    public class UserService
    {
        public string CurrentUserName { get; set; } = "Guest";
    }
}
LoggerService (Services/LoggerService.cs)

namespace BlazorSample.Services
{
    public class LoggerService
    {
        public void Log(string message)
        {
            Console.WriteLine(message);
        }
    }
}
CartService (Services/CartService.cs)

namespace BlazorSample.Services
{
    public class CartService
    {
        private readonly LoggerService _logger;
        private readonly UserService _userService;

        public CartService(LoggerService logger, UserService userService)
        {
            _logger = logger;
            _userService = userService;
        }

        public void Checkout()
        {
            _logger.Log($"Checkout by {_userService.CurrentUserName}");
        }
    }
}
Razor (Pages/CartPage.razor)
@page "/cart-page"
@inject BlazorSample.Services.CartService CartService

<h3>Cart Page</h3>

<button class="btn btn-success" @onclick="CartService.Checkout">
    Checkout
</button>
Program.cs

builder.Services.AddScoped<LoggerService>();
builder.Services.AddScoped<UserService>();
builder.Services.AddScoped<CartService>();

5️⃣ Phân biệt Scoped, Singleton, Transient

Đây là 3 lifetime chính khi đăng ký service vào DI container.

  • Scoped: Mỗi scope sẽ có một instance riêng. Trong Blazor Server, thường hiểu là mỗi user / mỗi circuit sẽ có một instance riêng. Đây là lựa chọn phù hợp nhất cho các service giữ state theo từng người dùng, ví dụ: giỏ hàng, bộ lọc tìm kiếm, form nhiều bước, state tạm thời của phiên làm việc.
  • Singleton: Chỉ có một instance duy nhất cho toàn bộ ứng dụng. Mọi component và mọi user đều dùng chung instance này. Phù hợp cho cấu hình chung, cache dùng chung, hoặc dữ liệu toàn hệ thống. Tuy nhiên, trong Blazor Server, không nên dùng Singleton cho state theo từng user, vì dữ liệu có thể bị chia sẻ giữa nhiều người dùng.
  • Transient: Mỗi lần inject hoặc resolve sẽ tạo một instance mới. Phù hợp cho các service tạm thời, ít giữ state, hoặc chỉ dùng để xử lý logic ngắn hạn. Không phù hợp nếu bạn muốn nhiều component cùng chia sẻ một state chung.

Trong thực tế: Scoped thường là lựa chọn mặc định tốt nhất cho đa số service trong Blazor Server; Singleton chỉ nên dùng khi bạn thật sự muốn chia sẻ dữ liệu toàn app; còn Transient phù hợp cho object xử lý tạm thời, không cần lưu trạng thái dùng chung.

Xem code
Các service ví dụ

public class CounterService { }

public class AppConfigService { }

public class TempCalculationService { }
Program.cs

builder.Services.AddScoped<CounterService>();
builder.Services.AddSingleton<AppConfigService>();
builder.Services.AddTransient<TempCalculationService>();

6️⃣ Đăng ký bằng factory khi constructor có tham số không phải service

Nếu constructor có string, int hoặc cấu hình riêng, DI không tự resolve được. Lúc này dùng factory registration.

Xem code
LoggerService (Services/LoggerService.cs)

namespace BlazorSample.Services
{
    public class LoggerService
    {
        public void Log(string message)
        {
            Console.WriteLine(message);
        }
    }
}
ApiClientService (Services/ApiClientService.cs)

namespace BlazorSample.Services
{
    public class ApiClientService
    {
        private readonly LoggerService _logger;
        private readonly string _baseUrl;

        public ApiClientService(LoggerService logger, string baseUrl)
        {
            _logger = logger;
            _baseUrl = baseUrl;
        }

        public string GetInfo()
        {
            _logger.Log($"BaseUrl = {_baseUrl}");
            return _baseUrl;
        }
    }
}
Razor (Pages/ApiClientPage.razor)
@page "/api-client-page"
@inject BlazorSample.Services.ApiClientService ApiClientService

<h3>Api Client Page</h3>

<p>Base URL: @ApiClientService.GetInfo()</p>
Program.cs

builder.Services.AddScoped<LoggerService>();

builder.Services.AddScoped<ApiClientService>(sp =>
{
    var logger = sp.GetRequiredService<LoggerService>();
    return new ApiClientService(logger, "https://api.example.com");
});

7️⃣ Inject built-in service của Blazor

Ngoài service tự tạo, bạn còn có thể inject các service có sẵn của framework.

Xem code
Razor (Pages/BuiltInServiceDemo.razor)
@page "/built-in-service-demo"
@inject NavigationManager Nav
@inject IJSRuntime JS
@inject ILogger<BuiltInServiceDemo> Logger
@inject HttpClient Http

<h3>Built-in Services</h3>

<button class="btn btn-primary" @onclick="GoHome">Go Home</button>

@code {
    private void GoHome()
    {
        Logger.LogInformation("Navigate to home");
        Nav.NavigateTo("/");
    }
}
Program.cs

// NavigationManager, IJSRuntime, ILogger... là built-in service
// Không cần tự AddScoped/AddSingleton cho chúng

8️⃣ Component con inject service trực tiếp

Component con có thể inject trực tiếp service từ DI, không cần parent truyền qua parameter.

Xem code
Service (Services/CounterService.cs)

namespace BlazorSample.Services
{
    public class CounterService
    {
        public int Count { get; private set; }

        public void Increment()
        {
            Count++;
        }
    }
}
Parent (Pages/ParentPage.razor)
@page "/parent-page"

<h3>Parent Page</h3>

<CounterPanel />
Child (Components/CounterPanel.razor)
@inject BlazorSample.Services.CounterService CounterService

<h4>Counter Panel</h4>

<p>Count: @CounterService.Count</p>

<button class="btn btn-primary" @onclick="CounterService.Increment">
    Increment
</button>
Program.cs

builder.Services.AddScoped<CounterService>();

9️⃣ State service có event OnChange

Đây là pattern rất hay gặp trong state management bằng DI service.

Xem code
State Service (Services/CounterStateService.cs)

namespace BlazorSample.Services
{
    public class CounterStateService
    {
        public int Count { get; private set; }

        public event Action? OnChange;

        public void Increment()
        {
            Count++;
            OnChange?.Invoke();
        }
    }
}
Razor (Pages/CounterStatePage.razor)
@page "/counter-state-page"
@implements IDisposable
@inject BlazorSample.Services.CounterStateService CounterState

<h3>Counter State Page</h3>

<p>Count: @CounterState.Count</p>

<button class="btn btn-primary" @onclick="CounterState.Increment">
    Increment
</button>

@code {
    protected override void OnInitialized()
    {
        CounterState.OnChange += HandleChange;
    }

    private void HandleChange()
    {
        InvokeAsync(StateHasChanged);
    }

    public void Dispose()
    {
        CounterState.OnChange -= HandleChange;
    }
}
Program.cs

builder.Services.AddScoped<CounterStateService>();

🔟 Dùng interface với DI

Đây là cách phổ biến trong dự án lớn để dễ thay thế implementation và dễ unit test.

Xem code
Interface (Services/IMessageService.cs)

namespace BlazorSample.Services
{
    public interface IMessageService
    {
        string GetMessage();
    }
}
Implementation (Services/MessageService.cs)

namespace BlazorSample.Services
{
    public class MessageService : IMessageService
    {
        public string GetMessage()
        {
            return "Hello from MessageService";
        }
    }
}
Razor (Pages/MessagePage.razor)
@page "/message-page"
@inject BlazorSample.Services.IMessageService MessageService

<h3>Message Page</h3>

<p>@MessageService.GetMessage()</p>
Program.cs

builder.Services.AddScoped<IMessageService, MessageService>();

1️⃣1️⃣ Tổng kết nên dùng cách nào

  • @inject: dùng trong Razor component/page.
  • [Inject]: dùng trong @code khi muốn code gọn hơn.
  • Constructor injection: dùng trong service/class C# thông thường.
  • Factory registration: dùng khi constructor có tham số không phải service.
  • Interface registration: nên dùng trong dự án lớn, dễ test và dễ thay implementation.

1️⃣2️⃣ Lưu ý quan trọng khi dùng DI

  • Không dùng Singleton cho state theo từng user trong Blazor Server nếu không hiểu rõ hậu quả.
  • Không để component sửa trực tiếp dữ liệu nội bộ của service một cách tự do.
  • Nếu subscribe event từ service, nhớ unsubscribe trong Dispose().
  • Service không nên biết UI; component mới là nơi gọi StateHasChanged().
An error has occurred. This application may no longer respond until reloaded. Reload 🗙
Web hosting by Somee.com