泛型工廠#
此模式應用於 Microsoft.Extensions.* 和 Microsoft.AspNetCore.*。思路是,可以使用泛型類型作為工廠而不是函數。泛型的參數就是要實例化的類型。比如,我們有一個 IServiceFactory<TService>
,那麼可以從 DI 容器中解析 TService
或創建實例(如果它不在容器中)。
public interface IServiceFactory<TService>
{
TService Service { get; }
}
public class ServiceFactory<TService> : IServiceFactory<TService>
{
public ServiceFactory(IServiceProvider provider)
{
Service = (TService)provider.GetService(typeof(TService)) ?? ActivatorUtilities.CreateInstance<TService>(provider);
}
public TService Service { get; }
}
構造函數可以訪問任何服務和被請求的泛型類型。這些開放的泛型服務是這樣註冊的:
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient(typeof(IServiceFactory<>), typeof(ServiceFactory<>));
}
延遲初始化服務#
有時,需要在構造函數執行之後創建服務。通常的做法是將 IServiceProvider
注入到需要延遲創建實例的服務的構造函數中。
public class Service
{
private readonly IServiceProvider _serviceProvider;
public Service(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public IFoo CreateFoo() => _serviceProvider.GetRequiredService<IFoo>();
public IBar CreateBar() => _serviceProvider.GetRequiredService<IBar>();
}
如果提前知道類型,我們可以構建一個 custom service 來提供延遲類型用於封裝服務定位器模式(service locator pattern):
public interface ILazy<T>
{
T Value { get; }
}
public class LazyFactory<T> : ILazy<T>
{
private readonly Lazy<T> _lazy;
public LazyFactory(IServiceProvider service)
{
_lazy = new Lazy<T>(() => service.GetRequiredService<T>());
}
public T Value => _lazy.Value;
}
public class Service
{
private readonly ILazy<IFoo> _foo;
private readonly ILazy<IBar> _bar;
public Service(ILazy<IFoo> foo, ILazy<IBar> bar)
{
_foo = foo;
_bar = bar;
}
public IFoo CreateFoo() => _foo.Value;
public IBar CreateBar() => _bar.Value;
}
Lazy 類型只有在訪問 .Value
屬性時才會實例化類型,可以提高性能。
開放泛型是這樣註冊的:
public void ConfigureServices(IServiceCollection services)
{
// We register this as transient so it handles both singleton and scoped services
// as the IServiceProvider injected will have the relevant lifetime.
services.AddTransient(typeof(ILazy<>), typeof(LazyFactory<>));
}
如果希望在註冊服務時添加具體類型,也可以直接使用 Lazy<T>
:
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<Lazy<IFoo>>(sp => new Lazy<IFoo>(() => sp.GetRequiredService<IFoo>()));
services.AddTransient<Lazy<IBar>>(sp => new Lazy<IBar>(() => sp.GetRequiredService<IBar>()));
}
繼承多個接口的類型#
假設有一個實現類型(class)繼承了多個接口(interface),並且想通過 ID 容器公開它。內置的 IServiceCollection
不支持,但使用以下模式。
public class FooAndBar : IFoo, IBar
{
// Imagine a useful implementation
}
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<FooAndBar>();
services.AddSingleton<IFoo>(sp => sp.GetRequiredService<FooAndBar>());
services.AddSingleton<IBar>(sp => sp.GetRequiredService<FooAndBar>());
}
這樣在解析 FooAndBar
、IFoo
和 IBar
時將會得到同樣的實例。
使用 IServiceProvider 實例化類型#
通常需要註冊類型才能從 DI 容器中獲取該類型的實例,比如 IServiceProvider.GetService
。這意味著服務需要在容器中註冊。如果有些類型是動態發現的(如 MVC 中的控制器),那就無法從 DI 容器中解析出來。
不過好在微軟提供了 ActivatorUtilities,它是一個類型工廠,它從 IServiceProvider
中解析出指定類型的實例,假如該類型沒有在 DI 容器中註冊,它會直接創建出這個類型的實例。
public class MyDependency
{
public MyDependency(ILogger logger) {}
}
public class MyDependencyFactory
{
private readonly IServiceProvider _serviceProvider;
public MyDependencyFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public MyDependency GetInstance()
{
return ActivatorUtilities.CreateInstance<MyDependency>(_serviceProvider);
}
}
可以用 CreateFactory
構建更優化的版本,它會預先計算指定類型的構造函數,並為其構建一個工廠方法(它返回一個委託)。
public class MyDependencyFactory
{
private readonly IServiceProvider _serviceProvider;
private readonly ObjectFactory _factory;
public MyDependencyFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
_factory = ActivatorUtilities.CreateFactory(typeof(MyDependency), Type.EmptyTypes);
}
public MyDependency GetInstance()
{
return (MyDependency) _factory(_serviceProvider, null);
}
}
Warning
DI 容器不會釋放使用此 API 創建的一次性實例。
所以記得手動 Dispose()
釋放資源,否則可能這些實例可能會一直佔用內存,導致內存洩漏。
https://github.com/davidfowl/DotNetCodingPatterns/issues/13
所以最好這樣使用:
using (var scope = serviceProvider.CreateScope())
{
var dbContext = scope.ServiceProvider.GetService<DBContext>();
// ...
}
在泛型類型中緩存單例#
如果需要緩存某個類型的實例,可以將它存儲在泛型類型的静态字段中。
public class Factory
{
public T Create<T>() where T : new()
{
return Cache<T>.Instance;
}
private static class Cache<T> where T : new()
{
public static readonly T Instance = new();
}
}
可以使用 JIT 代替較慢的 ConcurrentDictionary<Type, T>
來緩存實例。它還可用於緩存昂貴的對象創建過程:
public class Factory
{
public T Create<T>() where T : new()
{
return Cache<T>.Instance;
}
private static class Cache<T> where T : new()
{
public static readonly T Instance = CallExpensiveMethodToBuildANewInstance();
private static T CallExpensiveMethodToBuildANewInstance()
{
// 假設這裡經歷了非常複雜的過程,才得到了類型 T 的實例
return instanceOfT;
}
}
}
使用 IOptions 時訪問 DI 容器中的服務#
options 模式應用於 Microsoft.Extensions.*
和 Microsoft.AspNetCore.*
。它允許任何人註冊回調用於更新配置庫的 POCO。有時我們希望在配置 options 時訪問服務,有多種方法可以做到:
public class LibraryOptions
{
public int Setting { get; set; }
}
public class MyConfigureOptions : IConfigureOptions<LibraryOptions>
{
private readonly ISomeService _service;
public MyConfigureOptions(ISomeService service)
{
_service = service;
}
public void Configure(LibraryOptions options)
{
options.Setting = _service.ComputeSetting();
}
}
然後註冊:
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IConfigureOptions<LibraryOptions>, MyConfigureOptions>();
}
不過這種寫法太繁瑣了,微軟提供了一些 helpers 來簡化這一過程:
public void ConfigureServices(IServiceCollection services)
{
services.AddOptions<LibraryOptions>()
.Configure<ISomeService>((options, service) =>
{
options.Setting = service.ComputeSetting();
});
}
異步 Socket Server#
下面是一個使用現代化 .NET APIs 的異步 server:
- 基於 Task 接口,異步 accept/read/write
- 對於緩衝區進行切片的範圍語法
using var listenSocket = new Socket(SocketType.Stream, ProtocolType.Tcp);
listenSocket.Bind(new IPEndPoint(IPAddress.Loopback, 8080));
Console.WriteLine($"Listening on {listenSocket.LocalEndPoint}");
listenSocket.Listen();
while (true)
{
// Wait for a new connection to arrive
var connection = await listenSocket.AcceptAsync();
// We got a new connection spawn a task to so that we can echo the contents of the connection
_ = Task.Run(async () =>
{
var buffer = new byte[4096];
try
{
while (true)
{
int read = await connection.ReceiveAsync(buffer, SocketFlags.None);
if (read == 0)
{
break;
}
await connection.SendAsync(buffer[..read], SocketFlags.None);
}
}
finally
{
connection.Dispose();
}
});
}
異步 Socket Client#
下面是一個使用現代化 .NET APIs 的異步 client:
- 基於 Task 的接口,異步建立連接
- 通過 Socket 創建 NetworkStream 以使用 CopyToAsync,這是一個 helper,可以輕鬆地將內容從一個 Stream 複製到另一個 Stream。
using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
await socket.ConnectAsync(new IPEndPoint(IPAddress.Loopback, 8080));
Console.WriteLine("Type into the console to echo the contents");
var ns = new NetworkStream(socket);
var readTask = Console.OpenStandardInput().CopyToAsync(ns);
var writeTask = ns.CopyToAsync(Console.OpenStandardOutput());
// Quit if any of the tasks complete
await Task.WhenAny(readTask, writeTask);