1. 程式人生 > 實用技巧 >[ASP.NET Core開發實戰]基礎篇02 依賴注入

[ASP.NET Core開發實戰]基礎篇02 依賴注入

ASP.NET Core的底層機制之一是依賴注入(DI)設計模式,因此要好好掌握依賴注入的用法。

什麼是依賴注入

我們看一下下面的例子:

public class MyDependency
{
    public MyDependency()
    {
    }

    public Task WriteMessage(string message)
    {
        Console.WriteLine(
            $"MyDependency.WriteMessage called. Message: {message}");

        return Task.FromResult(0);
    }
}

public class IndexModel : PageModel
{
    MyDependency _dependency = new MyDependency();

    public async Task OnGetAsync()
    {
        await _dependency.WriteMessage(
            "IndexModel.OnGetAsync created this message.");
    }
}

IndexModel類直接在內部建立了一個MyDependency例項,直接依賴於MyDependency。在開發時避免使用這種方式,原因如下:

  • 要替換不同實現的MyDependency時,必須修改類。
  • 如果MyDependency也有依賴,必須由類對其進行配置。
  • 很難進行單元測試。

可以通過以下方式解決這些問題:

  • 使用介面或基類。
  • 註冊服務視窗中的依賴關注。
  • 通過建構函式注入。

將上面的例子改造為依賴注入方式:

public interface IMyDependency
{
    Task WriteMessage(string message);
}

public class MyDependency : IMyDependency
{
    private readonly ILogger<MyDependency> _logger;

    public MyDependency(ILogger<MyDependency> logger)
    {
        _logger = logger;
    }

    public Task WriteMessage(string message)
    {
        _logger.LogInformation(
            "MyDependency.WriteMessage called. Message: {MESSAGE}", 
            message);

        return Task.FromResult(0);
    }
}

public class IndexModel : PageModel
{
    MyDependency _dependency;
    public IndexModel(MyDependency dependency){
        _dependency = dependency;
    }

    public async Task OnGetAsync()
    {
        await _dependency.WriteMessage(
            "IndexModel.OnGetAsync created this message.");
    }
}

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }
    
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddScoped<IMyDependency, MyDependency>();
    }
}

在Startup裡註冊服務

服務可以在Startup.Configure註冊:

public void Configure(IApplicationBuilder app, IOptions<MyOptions> options)
{
    ...
}

使用擴充套件方法註冊服務

當使用服務集合的擴充套件方法來註冊服務,約定使用Add{SERVICE_NAME}擴充套件方法來註冊該服務所需的所有服務。

以下例子是使用擴充套件方法AddDbContextAddIdentity向容器新增服務。

public void ConfigureServices(IServiceCollection services)
{
    ...

    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();

    ...
}

服務生命週期

服務的生命週期有三種:Transient、Scoped、Singleton。

Transient

Transient,意為暫時的,是每次服務容器進行請求時建立的,即服務每次例項化時建立一次。這種生存期適合輕量級、無狀態的服務。

Scoped

Scoped,意為範圍內的,每個客戶端請求(連線)時會建立一次。以Web為例,即一次Http請求內建立一次且請求內有效

Singleton

Singleton,意為單例的,是在第一次請求時建立的。後面每次請求時使用的是同一個例項。在應用關閉,釋放ServiceProvider時,會釋放。

驗證服務生命週期

下面示例是演示服務生命週期的差異。

public interface IOperation
{
    Guid OperationId { get; }
}

public interface IOperationTransient : IOperation
{
}

public interface IOperationScoped : IOperation
{
}

public interface IOperationSingleton : IOperation
{
}

public interface IOperationSingletonInstance : IOperation
{
}

public class Operation : IOperationTransient, 
    IOperationScoped, 
    IOperationSingleton, 
    IOperationSingletonInstance
{
    public Operation() : this(Guid.NewGuid())
    {
    }

    public Operation(Guid id)
    {
        OperationId = id;
    }

    public Guid OperationId { get; private set; }
}

public class OperationService
{
    public OperationService(
        IOperationTransient transientOperation,
        IOperationScoped scopedOperation,
        IOperationSingleton singletonOperation,
        IOperationSingletonInstance instanceOperation)
    {
        TransientOperation = transientOperation;
        ScopedOperation = scopedOperation;
        SingletonOperation = singletonOperation;
        SingletonInstanceOperation = instanceOperation;
    }

    public IOperationTransient TransientOperation { get; }
    public IOperationScoped ScopedOperation { get; }
    public IOperationSingleton SingletonOperation { get; }
    public IOperationSingletonInstance SingletonInstanceOperation { get; }
}


在Startup.ConfigureServices中,指定各服務的生命週期。

public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();

    services.AddScoped<IMyDependency, MyDependency>();
    services.AddTransient<IOperationTransient, Operation>();
    services.AddScoped<IOperationScoped, Operation>();
    services.AddSingleton<IOperationSingleton, Operation>();
    services.AddSingleton<IOperationSingletonInstance>(new Operation(Guid.Empty));

    // OperationService depends on each of the other Operation types.
    services.AddTransient<OperationService, OperationService>();
}

示例IndexModel:

public class IndexModel : PageModel
{
    private readonly IMyDependency _myDependency;

    public IndexModel(
        IMyDependency myDependency, 
        OperationService operationService,
        IOperationTransient transientOperation,
        IOperationScoped scopedOperation,
        IOperationSingleton singletonOperation,
        IOperationSingletonInstance singletonInstanceOperation)
    {
        _myDependency = myDependency;
        OperationService = operationService;
        TransientOperation = transientOperation;
        ScopedOperation = scopedOperation;
        SingletonOperation = singletonOperation;
        SingletonInstanceOperation = singletonInstanceOperation;
    }

    public OperationService OperationService { get; }
    public IOperationTransient TransientOperation { get; }
    public IOperationScoped ScopedOperation { get; }
    public IOperationSingleton SingletonOperation { get; }
    public IOperationSingletonInstance SingletonInstanceOperation { get; }

    public async Task OnGetAsync()
    {
        await _myDependency.WriteMessage(
            "IndexModel.OnGetAsync created this message.");
    }
}

以下是兩次訪問IndexModel的結果:

第一個請求:
控制器操作:
暫時性:d233e165-f417-469b-a866-1cf1935d2518
作用域:5d997e2d-55f5-4a64-8388-51c4e3a1ad19
單一例項:01271bc1-9e31-48e7-8f7c-7261b040ded9
例項:00000000-0000-0000-0000-000000000000
OperationService 操作:
暫時性:c6b049eb-1318-4e31-90f1-eb2dd849ff64
作用域:5d997e2d-55f5-4a64-8388-51c4e3a1ad19
單一例項:01271bc1-9e31-48e7-8f7c-7261b040ded9
例項:00000000-0000-0000-0000-000000000000

第二個請求:
控制器操作:
暫時性:b63bd538-0a37-4ff1-90ba-081c5138dda0
作用域:31e820c5-4834-4d22-83fc-a60118acb9f4
單一例項:01271bc1-9e31-48e7-8f7c-7261b040ded9
例項:00000000-0000-0000-0000-000000000000
OperationService 操作:
暫時性:c4cbacb8-36a2-436d-81c8-8c1b78808aaf
作用域:31e820c5-4834-4d22-83fc-a60118acb9f4
單一例項:01271bc1-9e31-48e7-8f7c-7261b040ded9
例項:00000000-0000-0000-0000-000000000000

從上面的結果,觀察OperationId的變化:

  • 暫時性,值始終不同。
  • 作用域,同一請求內相同,不同請求不同。
  • 單例,不管是同一請求還是不同請求,都一樣。

最佳實踐

最佳做法:

  • 設計服務以使用依賴關係注入來獲取其依賴關係。
  • 避免有狀態的、靜態類和成員。將應用設計為改用單一例項服務,可避免建立全域性狀態。
  • 避免在服務中直接例項化依賴類。 直接例項化將程式碼耦合到特定實現。
  • 不在應用類中包含過多內容,確保設計規範,並易於測試。