KHÔNG GIAN HỌC TẬP / DI Service
Danh mục bài học Blazor Server .NET 10
BÀI 08Component và trạng thái

DI Service

Đăng ký service một lần, nhận đúng phụ thuộc ở nơi cần dùng và chọn vòng đời phù hợp cho dữ liệu của ứng dụng.

Giải thích, ví dụ & thực hành Tất cả bài học

Tra cứu các cách dùng dependency injection thường gặp trong Blazor: nhận service trong component, truyền phụ thuộc qua constructor, đăng ký bằng factory và thay thế implementation qua interface.

Cách đọc bài này. Mỗi mục có ví dụ service, nơi sử dụng và phần đăng ký trong Program.cs. Bắt đầu với @inject, sau đó đọc phần vòng đời để chọn Scoped, Singleton hay Transient. Các khối mã là ví dụ để đối chiếu, không phải những service đều đã được đăng ký trong project này.

01. Nhận service bằng @inject

Đâ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>();

02. Nhận service qua thuộc tính [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>();

03. Truyền phụ thuộc qua constructor

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>();

04. Một service sử dụng nhiều phụ thuộ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>();

05. Chọn vòng đời của service

Đâ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>();

06. Đăng ký bằng factory

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");
});

07. Sử dụng service có sẵn

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

08. Nhận service ngay trong component con

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>();

09. Thông báo thay đổi bằng 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>();

10. Đăng ký thông qua interface

Đâ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>();

11. Chọn cách nhận service

  • @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.

12. Những điều cần lưu ý

  • 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().
Quy tắc thực hành

Chọn vòng đời theo phạm vi dữ liệu cần giữ, không theo việc cách đăng ký nào ngắn hơn. Với state riêng của từng phiên Blazor Server, tránh dùng Singleton để không chia sẻ dữ liệu ngoài ý muốn.

Web hosting by Somee.com