ジェネリックファクトリー#
このパターンは 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>();
}
タイプが事前にわかっている場合、サービスロケーターパターンをカプセル化するために遅延タイプを提供するカスタムサービスを構築できます:
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)
{
// これはトランジェントとして登録されるので、シングルトンとスコープ付きサービスの両方を処理します
// 注入された IServiceProvider は関連するライフタイムを持ちます。
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>()));
}
複数のインターフェースを継承する型#
複数のインターフェースを継承する実装型(クラス)があり、ID コンテナを通じて公開したいとします。組み込みの IServiceCollection
はサポートしていませんが、次のパターンを使用します。
public class FooAndBar : IFoo, IBar
{
// 有用な実装を想像してください
}
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 コンテナから解決することはできません。
しかし、Microsoft は 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();
}
}
インスタンスをキャッシュするために、遅い ConcurrentDictionary<Type, T>
の代わりに JIT を使用できます。これは高価なオブジェクト作成プロセスをキャッシュするためにも使用できます:
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 コンテナ内のサービスにアクセスする#
オプションパターンは Microsoft.Extensions.*
と Microsoft.AspNetCore.*
に適用されます。これにより、誰でも構成ライブラリを更新するためのコールバックを登録できます。時には、オプションを構成する際にサービスにアクセスしたいことがあります。これを行う方法はいくつかあります:
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>();
}
ただし、この書き方は煩雑すぎるため、Microsoft はこのプロセスを簡素化するためのいくつかのヘルパーを提供しています:
public void ConfigureServices(IServiceCollection services)
{
services.AddOptions<LibraryOptions>()
.Configure<ISomeService>((options, service) =>
{
options.Setting = service.ComputeSetting();
});
}
非同期ソケットサーバー#
以下は、最新の .NET API を使用した非同期サーバーの例です:
- タスクインターフェースに基づく非同期 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)
{
// 新しい接続が到着するのを待つ
var connection = await listenSocket.AcceptAsync();
// 新しい接続を取得し、接続の内容をエコーするタスクをスパーンします
_ = 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();
}
});
}
非同期ソケットクライアント#
以下は、最新の .NET API を使用した非同期クライアントの例です:
- タスクベースのインターフェース、非同期接続の確立
- ソケットを使用して NetworkStream を作成し、CopyToAsync を使用します。これは、あるストリームから別のストリームにコンテンツを簡単にコピーするためのヘルパーです。
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());
// いずれかのタスクが完了した場合は終了
await Task.WhenAny(readTask, writeTask);