ASP.NET COREでのグローバルクエリ結果のキャッシュ

何らかの形式のCQRSパラダイムでは、Query呼び出しがアプリケーションの状態を変更しないと想定しています。 つまり、同じクエリ内で同じクエリを複数回呼び出しても同じ結果になります。







クエリの一部として使用されるすべてのインターフェイスをIQueryまたはIAsyncQueryタイプにします。







public interface IQuery<TIn, TOut> { TOut Query(TIn input); } public interface IAsyncQuery<TIn, TOut>: IQuery<TIn, Task<TOut> { }
      
      





これらのインターフェースは、データの受け取り、たとえば割引/ボーナスおよびその他すべてを考慮したフォーマットされた価格の受け取りを完全に記述します。







 public class ProductPriceQuery: IQuery<ProductDto,PricePresentationDto> { public ProductPriceQuery( IQuery<ProductDto, PriceWithSalesDto> priceWithSalesQuery, IQuery<PriceWithSalesDto, PricePresentationDto> pricePresentationQuery) { _priceWithSalesQuery = priceWithSalesQuery; _pricePresentationQuery = pricePresentationQuery; } public PricePresentationDto Query(ProductDto dto) { var withSales = _priceWithSalesQuery(dto); var result = _pricePresentationQuery(withSales); return result; } }
      
      





パイプラインインターフェイス



このアプローチの利点は、アプリケーション内のインターフェースの均一性であり、パイプラインで構築できます。







 public class Aggregate2Query<TIn, TOut1, TOut2> : BaseAggregateQuery<TIn, TOut2> { public Aggregate2Query( IQuery<TIn, TOut1> query0, IQuery<TOut1, TOut2> query1) : base(query0, query1){} } public abstract class BaseAggregateQuery<TIn, TOut> : IQuery<TIn, TOut> { private object[] queries { get; set; } protected BaseAggregateQuery(params object[] queries) { this.queries = queries; } public TOut Query(TIn input) => queries.Aggregate<object, dynamic>(input, (current, query) => ((dynamic) query).Query(current)); }
      
      





次のように登録します。







 serviceCollection.AddScoped(typeof(Aggregate2Query<,,>));
      
      





取得するもの:







 public ProductPriceQuery( BaseAggregateQuery<ProductDto,PriceWithSalesDto,PricePresentationDto> query) { _aggregateQuery = query; } public PricePresentationDto Query(ProductDto dto) => _aggregateQuery.Query(dto);
      
      





理想的には、プログラミングはコンストラクターのアセンブリになりますが、実際にはプログラマーのプライドを満たすための美しいトリックにすぎません。







デコレータとASP.NET CORE



MediatRライブラリは、インターフェイスの均一性とデコレータ上に正確に構築されています。







デコレータを使用すると、ロギングなどの標準のIQuery <TIn、TOut>インターフェイスにいくつかの追加機能をハングさせることができます。







 public class LoggingQuery<TIn,TOut>: IQuery<TIn,TOut> { public LoggingQuery(IQuery<TIn,TOut> priceQuery) { _priceQuery = priceQuery } public TOut Query(TIn input) { Console.WriteLine($"Query {_priceQuery.GetType()} Start"); var result= _priceQuery.Query(input); Console.WriteLine($"Query {_priceQuery.GetType()} End"); return result; } }
      
      





デコレータを使用すると、プログラム全体に拡散するのではなく、横断的な機能を1か所で作成できるという事実は省略しますが、これはこの記事の範囲ではありません。







.Net Coreが提供する標準のIoCコンテナはデコレータを登録できません。難点は、同じインターフェイスの2つの実装:元のクエリとデコレータ、およびデコレータが実装する同じインターフェイスがデコレータコンストラクタに来ることです。 コンテナはこのようなグラフを解決できず、「循環依存」エラーをスローします。







この問題を解決する方法はいくつかありますが、特に.Net Coreコンテナの場合、Scrutorライブラリが作成され、デコレータを登録できます。







  services.Decorate(typeof(IQuery<,>), typeof(LoggingQuery<,>));
      
      





プロジェクトに追加の依存関係を追加したくない場合は、この機能を自分で記述できます。 コードを実演する前に、クエリ結果のクエリの一部としてのキャッシュについて説明しましょう。 キャッシュを追加する必要があり、クラスがキーである場合は、GetHashCodeとEqualsをオーバーライドする必要があります。そのため、参照による比較を取り除きます。







キャッシュ方法



単純なキャッシュの例を示します。







 //Cache ConcurrentDictionary<Key,Value> _cache { get; } //Key public class Key { //ReSharper-generated code protected bool Equals(Key other) { return Field1 == other.Field1 && Field2 == other.Field2; } //ReSharper-generated code public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; if (obj.GetType() != this.GetType()) return false; return Equals((Key) obj); } //ReSharper-generated code public override int GetHashCode() { unchecked { return (Field1 * 397) ^ Field2; } } public int Field1 { get; set; } public int Field2 { get; set; } } //Value irrelevant
      
      





値を検索する場合、まずGetHashCodeメソッドが呼び出されて必要なバスケットが検索され、次にバスケットに複数の要素がある場合、比較のためにEqualsが呼び出されます。 仕組みがよくわからない場合は、ご覧ください。







ReSharper自体がこれらのメソッドを生成しますが、IQuery <TIn、TOut>インターフェイス、および一般的にIQuery <TIn、TOut>インターフェイスを実装したプログラマーは、キャッシュをグローバルに実装します。SRPを忘れないでください。 したがって、再シャーパーによるメソッドの生成は適切ではありません。







エンドツーエンドの機能を扱う場合、AOPフレームワークが役に立ちます。 FodyのプラグインであるEqualsFodyは、ILを書き換え、EqualsAttribute属性でマークされたクラスのEqualsとGetHashCodeをオーバーライドします。







この属性で各Dtoにフラグを立てないようにするために、IQueryインターフェイスを少し書き直すことができます







 public IQuery<TIn,TOut> where TIn : CachedDto{ } [Equals] public class CachedDto{ }
      
      





これで、すべてのDtoが必要なメソッドを確実にオーバーライドし、各入力DTOに属性を追加する必要がなくなります(基本クラスから取得されます)。 ILの書き換えが適さない場合は、CachedDtoを次のように実装します(基本クラスメソッドが呼び出されるコンテキストを使用します)。







 public class CachedDto{ public override bool Equals(object x) => DeepEquals.Equals(this,x); public override int GetHashCode() => DeepHash.GetHashCode(this); }
      
      





DeepEquals.EqualsおよびDeepHash.GetHashCodeはリフレクションを使用します。Fodyよりも遅くなり、企業アプリケーションにとって致命的ではありません。







ただし、SRPについては覚えておいてください。IQueryはキャッシュされていることを知ってはいけません。







最善の解決策は、IEqualityComparerを実装することです。 辞書はコンストラクタでそれを受け取り、挿入/削除/検索で使用します。







  public class EqualityComparerUsingReflection<TKey> : IEqualityComparer<TKey> { public bool Equals(TKey x, TKey y) => DeepEqualsCommonType(x, y); public int GetHashCode(TKey obj) => Hash.GetHashCode(obj); }
      
      





これでTInに制約をスローできるようになり、目的を達成しました。 キャッシングデコレータを作成しましょう。







  public class BaseCacheQuery<TIn, TOut> : IQuery<TIn, TOut> { private readonly ConcurrentDictionary<TIn, TOut> _cache; private readonly IQuery<TIn, TOut> _query; protected BaseCacheQuery( IQuery<TIn, TOut> query, IConcurrentDictionaryFactory<TIn, TOut> factory) { _cache = factory.Create(); _query = query; } public TOut Query(TIn input) => _cache .GetOrAdd(input, x => _query.Query(input)); }
      
      





IConcurrentDictionaryFactoryに注意してください。このファクトリの目標は、辞書のインスタンスを提供することですが、コンストラクタで作成するだけではどうでしょうか。







最初に、DIおよびSRP、別の比較実装を追加する(たとえば、特定のタイプのDTOに簡単にする)か、実装を完全に変更する必要がある可能性が非常に高く、2番目に、キャッシュがリフレクションと抽象化によりスローダウンし始める状況が発生する可能性があります漏れている。 DtoでEqualsがオーバーライドされ、GetHashCodeが「重い」EqualityComparerを使用しない場合、妥協します。







ファクトリの目的は、メソッドがオーバーライドされているかどうかをチェックし、オーバーライドされている場合、DTOで再定義されたメソッドを使用して標準ディクショナリを返すことです。







登録



これをすべて登録する方法に戻りましょう。







ConfigureServicesメソッドのサービス引数はServiceDescriptorsのコレクションであり、各記述子には登録された依存関係に関する情報が含まれます







 public class ServiceDescriptor{ // other methods /// <inheritdoc /> public ServiceLifetime Lifetime { get; } /// <inheritdoc /> public Type ServiceType { get; } /// <inheritdoc /> public Type ImplementationType { get; } /// <inheritdoc /> public object ImplementationInstance { get; } /// <inheritdoc /> public Func<IServiceProvider, object> ImplementationFactory { get; } // other methods }
      
      





したがって、LifeTime = Scopedの新しいServiceDescriptorがサービスコレクションに追加されます。

ServiceType = typeof(IService)、ImplementType = typeof(Service):







 services.AddScoped<IService,Service>().
      
      





ImplementationFactoryプロパティを使用すると、依存関係の作成方法を指定できます;使用します。 IServiceCollectionの拡張機能を作成します。これにより、アセンブリ内のすべてのIQueryおよびIAsyncQueryが検索され、デコレータがハングアップして登録されます。







  public static void AddCachedQueries(this IServiceCollection serviceCollection) { // Func<Type,bool>    IAsyncQuery var asyncQueryScanPredicate = AggregatePredicates( IsClass, ContainsAsyncQueryInterface); // Func<Type,bool>     IQuery var queryScanAssemblesPredicate =AggregatePredicates( IsClass, x => !asyncQueryScanPredicate(x), ContainsQueryInterface); //    IAsyncQuery    var asyncQueries = GetAssemblesTypes( asyncQueryScanPredicate, DestAsyncQuerySourceType); //    IQuery    var queries = GetAssemblesTypes( queryScanAssemblesPredicate, DestQuerySourceType); //   ConcurrentDictionary serviceCollection.AddScoped( typeof(IConcurrentDictionaryFactory<,>), typeof(ConcDictionaryFactory<,>)); //   services ServiceDescriptor'   IAsyncQuery serviceCollection.QueryDecorate(asyncQueries, typeof(AsyncQueryCache<,>)); //   services ServiceDescriptor'   IQuery serviceCollection.QueryDecorate(queries, typeof(QueryCache<,>)); } private static void QueryDecorate(this IServiceCollection serviceCollection, IEnumerable<(Type source, Type dest)> parameters, Type cacheType, ServiceLifetime lifeTime = ServiceLifetime.Scoped) { foreach (var (source, dest) in parameters) serviceCollection.AddDecorator( cacheType.MakeGenericType(source.GenericTypeArguments), source, dest, lifeTime); } private static void AddDecorator( this IServiceCollection serviceCollection, Type cacheType, Type querySourceType, Type queryDestType, ServiceLifetime lifetime = ServiceLifetime.Scoped) { //ReSharper disable once ConvertToLocalFunction Func<IServiceProvider, object> factory = provider => ActivatorUtilities.CreateInstance(provider, cacheType, ActivatorUtilities.GetServiceOrCreateInstance(provider, queryDestType)); serviceCollection.Add( new ServiceDescriptor(querySourceType, factory, lifetime)); } }
      
      





AddDecoratorメソッドには特別な注意が必要です;ここでは、ActivatorUtilitiesクラスの静的メソッドを使用します。 ActivatorUtilities.CreateInstanceはIServiceProvider、作成されるオブジェクトのタイプ、およびこのオブジェクトがコンストラクターで受け入れる依存関係インスタンスを受け入れます(登録されていないもののみを指定でき、残りはプロバイダーによって許可されます)







ActivatorUtilities.GetServiceOrCreateInstance-同じことを行いますが、作成されたオブジェクトのコンストラクターに欠落している依存関係を渡すことはできません。 オブジェクトがコンテナに登録されている場合、オブジェクトを作成します(または、既に作成されているオブジェクトを返します)。そうでない場合は、すべての依存関係を解決できればオブジェクトを作成します。







したがって、キャッシュオブジェクトを返す関数を作成し、この登録を記述するサービスに記述子を追加できます。







テストを書きましょう:







 public class DtoQuery : IQuery<Dto, Something> { private readonly IRepository _repository; public DtoQuery(IRepository repository) { _repository = repository; } public Something Query(Dto input) => _repository.GetSomething(); } //    private IQuery<Dto, Something> query { get; set; } public void TwoCallQueryTest() { var dto = new Dto {One = 1}; var dto1 = new Dto {One = 1}; //query -        query.Query(dto); query.Query(dto1); // : services.AddScoped<IRepository>(x => MockRepository.Object) RepositoryMock.Verify(x => x.GetSomething(), Times.Once); }
      
      





ReposityMock-Moqライブラリのモックは面白いですが、リポジトリのGetSomething()メソッドが何回呼び出されたかをテストするために、デコレータも使用しますが、Castle.Interceptorを使用して自動的に生成します。 デコレータを使用してデコレータをテストします。





これは、すべてのIQuery <TIn、TOut>の結果のキャッシュを追加する方法です。小さな機能を実装するために大量のコードを記述するのは非常に不便です。









その他の解決策



Mediatr



中央ライブラリインターフェイス:







 public interface IRequestHandler<in TRequest, TResponse> where TRequest : IRequest<TResponse> { Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken); }
      
      





MediatRの主な機能は、IRequestHandlerにラッパーを追加することです。たとえば、IPipelineBehaviorインターフェイスを使用してパイプラインを実装します。これは、CachePipelineBehaviourを登録する方法です。登録されたすべてのIRequestHandlerインターフェイスに適用されます:







 sc.AddScoped(typeof(IPipelineBehavior<,>), typeof(CachePipelineBehaviour<,>));
      
      





キャッシングPipelineBehaviourを実装します。







 public class CachePipelineBehaviour<TDto, TResult> : IPipelineBehavior<TDto, TResult> { private readonly ConcurrentDictionary<TDto, Task<TResult>> _cache; public CachePipelineBehaviour( IConcurrentDictionaryFactory<TDto, Task<TResult>> cacheFactory) { _cache = cacheFactory.Create(); } public async Task<TResult> Handle(TDto request, CancellationToken cancellationToken, RequestHandlerDelegate<TResult> next) => await _cache.GetOrAdd(request, x => next()); }
      
      





要求のDto、キャンセルトークン、およびRequestHandlerDelegateがHandleメソッドに送られます。 後者は、他のデコレータとハンドラの次の呼び出しの単なるラッパーです。 MediatRはアセンブリをスキャンし、インターフェイス自体のすべての実装を登録します。 使用するには、IMediatorを挿入し、Dtoを渡してSendメソッドを呼び出す必要があります。







 public async Task<IActionResult>([FromBody] Dto dto){ return Ok(mediator.Send(dto)); }
      
      





MediatR自体がそれを見つけ、IRequestHabdlerの適切な実装を見つけ、すべてのデコレーターを適用します(PipelineBehaviourに加えて、IPreRequestHandlerとIPostRequestHandlerもあります)







キャッスルウィンザー



コンテナの機能は、動的ラッパーの生成です。これは動的AOPです。







Entity Frameworkはこれをレイジーロードに使用します。プロパティゲッターでは、ILazyLoaderインターフェイスのLoadメソッドが呼び出され、 コンストラクターの実装を介してエンティティのすべてのラッパーのクラスに注入されます。







ラッパーの生成を使用してコンテナーを構成するには、インターセプターを作成して登録する必要があります







  public class CacheInterceptor<TIn, TOut> : IInterceptor { private readonly ConcurrentDictionary<TIn, TOut> _cache; public CacheInterceptor( IConcurrentDictionaryFactory<TIn, TOut> cacheFactory) { _cache = cacheFactory.Create(); } public void Intercept(IInvocation invocation) { var input = (TIn) invocation.Arguments.Single(); if (_cache.TryGetValue(input, out var value)) invocation.ReturnValue = value; else { invocation.Proceed(); _cache.TryAdd(input, (TOut) invocation.ReturnValue); } } }
      
      





IInvocationインターフェイスは、アクセスされた装飾されたオブジェクトのメンバーに関する情報を提供します。インターフェイスの唯一のパブリックメンバーはQueryメソッドであるため、アクセスが登録されたことを確認せず、他のオプションはありません。







キャッシュにそのようなキーを持つオブジェクトがある場合は、メソッドの戻り値を(呼び出さずに)入力し、そうでない場合は、Proceedメソッドを呼び出します。Proceedメソッドは、装飾されたメソッドを呼び出し、ReturnValueを入力します。







インターセプターの登録と完全なコードはGithabで見ることができます








All Articles