在之前的依賴注入文章的「組合根請稍作分類」小節,我們介紹過使用 IServiceCollection 的擴充方法來對要註冊的服務進行分類和管理的做法:

public static class ServiceCollectionExtensions
{
    /// <summary>
    /// 註冊 Nice Service
    /// </summary>
	public static IServiceCollection AddNiceServices(this IServiceCollection services)
	{
		services.AddScoped<INiceService, NiceService>();
        // ...一些相關的註冊

		return services;
	}
}
// Program.cs
builder.Services.AddNiceServices();

一直以來我都採用這個方法來簡單地拆分我的服務註冊,在大多數的場合已經足夠使用(尤其是只關注目前的專案時)。

但前幾天在看某個套件的實作時,開發的朋朋跟我分享了一些延伸的做法,感覺合理又常見,屬於有注意到就會記得的小技巧,決定馬上來記錄一篇筆記。


場景是這樣的:當我們在開發套件的時候,常常會有主要套件跟擴充用的延伸套件。

例如說,我可能會有一個主要的 NiceTool 套件,跟擴展前者的 NiceTool.Memory 套件

這兩個套件都會需要對 IServiceCollection 註冊一些服務,並且延伸套件 NiceTool.Memory 需要確認 NiceTool 的服務都有進行註冊才能正常運作。

如果我們想要先確保主要套件 NiceTool 的服務都註冊了,再處理 NiceTool.Memory,同時又想保留擴充組合的彈性給以後的 NiceTool.SqlServer,甚至我們可能想在兩者各自的註冊加點工、傳遞點資訊,可以怎麼辦呢?

這時候就可以考慮包一個自家的 Builder,用這個 Builder 來把這些套件給串起來

以上面的例子來說,我可以製作一個 INiceToolBuilder,並且把服務註冊時要用到的 IServiceCollection 包在裡面:

public interface INiceToolBuilder
{
	IServiceCollection Services { get; }
}

public class NiceToolBuilder
{
    public IServiceCollection Services { get; }

	public NiceToolBuilder(IServiceCollection services)
    {
        Services = services;
    }
}

接著,在作為註冊起點的主要套件 NiceTool 裡,我就可以提供一個產生 INiceToolBuilder 的擴充方法:

public static INiceToolBuilder AddNiceTools(this IServiceCollection services)
{
	services.AddScoped<INiceService, NiceService>();
	// ...一些相關的註冊

	var builder = new NiceToolBuilder(services);
	return builder;
}

接著,在延伸的 NiceTool.Memory,我就可以接續著使用 INiceToolBuilder 來對 IServiceCollection 進行服務註冊:

public static INiceToolBuilder AddNiceMemoryTools(this INiceToolBuilder builder)
{
	// 對 INiceToolBuilder 帶進來的 IServiceCollection 繼續進行服務註冊
	builder.Services.AddScoped<INiceMemoryService, NiceMemoryService>();
	// ...一些相關的註冊

	return builder;
}

如此一來,我們就可以明示使用者在註冊的時候,先把主要套件提供的服務註冊方法呼叫完並取得 INiceToolBuilder,再進行延伸套件的服務註冊。如果有什麼資訊需要帶著一起走,也可以直接包在裡面就好。

Program.cs 也可以用我們熟悉的串串樂舒暢地一路串完,並讓使用者自行組合:

builder.Services
	.AddNiceServices()
	.AddNiceMemoryTools();

是不是看起來乾淨舒服,又方便有約束力呢。


為什麼會說這是「有注意到就會記得的小技巧」呢?因為這個做法其實還蠻常見的,尤其是各種套件和工具。

例如大家應該都很熟悉的 AddHealthChecks(),回傳的就是 IHealthChecksBuilder,後續就能接著去呼叫另一顆套件的 AddSqlServer()

又或者是我們 IOptions 文章介紹過的 AddOptions(),回傳的是 OptionsBuilder
隔壁的 AddLogging() 提供傳入 ILoggingBuilder 的委派作為參數等等……

如果只是一路 Add() Use() Add() Use(),可能就不會注意到這個設計上的小巧思。

像我個人以前比較少做套件,大多時候都在服務內處理,習慣 IServiceCollection 幹到底,就不曾留心在這部份過。這次有朋朋跟我分享了這個眉角,馬上跟之前註冊各種套件工具時的記憶連了起來。

延伸想了一下,同樣的概念也不是只有套件開發的時候會用到,那不如記錄下來,以後才方便抄(?)

如此如此,這般這般,又成功靠偷朋朋的小技巧水了一篇,功德圓滿,阿彌陀佛。