myesn

myEsn2E9

hi
github

.NET 編碼模式

泛型工廠#

此模式應用於 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>());
}

這樣在解析 FooAndBarIFooIBar 時將會得到同樣的實例。

使用 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);

參考#

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。