Caching Hasil Kueri Global di ASP.NET CORE

Paradigma CQRS dalam satu bentuk atau lainnya mengasumsikan bahwa panggilan Query tidak akan mengubah status aplikasi. Artinya, beberapa panggilan ke kueri yang sama dalam kueri yang sama akan memiliki hasil yang sama.


Biarkan semua antarmuka yang digunakan sebagai bagian dari kueri berupa tipe IQuery atau IAsyncQuery:


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

Antarmuka ini sepenuhnya menggambarkan penerimaan data, misalnya, penerimaan harga yang diformat dengan mempertimbangkan diskon / bonus akun dan yang lainnya:


 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; } } 

Antarmuka pipa


Keuntungan dari pendekatan ini adalah keseragaman antarmuka dalam aplikasi, yang dapat dibangun di dalam pipa:


 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)); } 

Daftarkan seperti ini:


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

Kami mendapatkan:


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

Idealnya, pemrograman harus berubah menjadi perakitan konstruktor, tetapi pada kenyataannya itu hanya trik yang indah untuk memuaskan kebanggaan programmer.


Dekorator dan ASP.NET CORE


Perpustakaan MediatR dibangun tepat pada keseragaman antarmuka dan dekorator.


Dekorator memungkinkan Anda untuk menggantung beberapa fungsi tambahan pada antarmuka standar IQuery <TIn, TOut>, misalnya masuk:


 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; } } 

Saya akan menghilangkan fakta bahwa dekorator memungkinkan Anda untuk menulis fungsionalitas lintas bidang di satu tempat, daripada menyebar di seluruh program, ini bukan bagian dari ruang lingkup artikel ini.


Kontainer IoC standar yang disediakan oleh .Net Core tidak dapat mendaftarkan dekorator. Kesulitannya adalah kita memiliki dua implementasi dari antarmuka yang sama: kueri asli dan dekorator, dan antarmuka yang sama dengan yang diterapkan oleh dekorator pada konstruktor dekorator. Wadah tidak dapat menyelesaikan grafik seperti itu dan melempar kesalahan "ketergantungan melingkar".


Ada beberapa cara untuk mengatasi masalah ini. Khusus untuk wadah .Net Core, perpustakaan Scrutor ditulis, dapat mendaftarkan dekorator:


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

Jika Anda tidak ingin menambahkan dependensi tambahan ke proyek, maka Anda dapat menulis sendiri fungsionalitas ini, yang saya lakukan. Sebelum mendemonstrasikan kode, mari kita membahas caching hasil Query sebagai bagian dari kueri. Jika Anda perlu menambahkan caching dan kuncinya adalah kelasnya, Anda harus mengganti GetHashCode dan Persamaan, jadi kami akan menyingkirkan perbandingan dengan referensi.


Metode Caching


Saya akan menyajikan contoh cache sederhana:


 //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 

Saat mencari nilai, metode GetHashCode pertama kali dipanggil untuk menemukan keranjang yang diinginkan, dan kemudian, jika keranjang memiliki lebih dari satu elemen, Equals dipanggil untuk perbandingan. Lihat apakah Anda tidak mengerti cara kerjanya.


ReSharper sendiri menghasilkan metode ini, tetapi kami menerapkan caching secara global, programmer yang mengimplementasikan antarmuka IQuery <TIn, TOut> dan, secara umum, antarmuka IQuery <TIn, TOut>, jangan lupa tentang SRP. Oleh karena itu, pembuatan metode oleh penyelam tidak sesuai dengan kita.


Ketika kita berurusan dengan fungsi ujung ke ujung, kerangka kerja AOP datang untuk menyelamatkan. EqualsFody, sebuah plugin untuk Fody, menulis ulang IL, menimpa Equals dan GetHashCode di kelas yang ditandai dengan atribut EqualsAttribute.


Agar tidak menandai setiap Dto dengan atribut ini, kita dapat menulis ulang antarmuka IQuery sedikit


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

Sekarang semua Dto pasti akan menimpa metode yang diperlukan, dan kita tidak perlu menambahkan atribut ke setiap input DTO (itu akan diambil dari kelas dasar). Jika menulis ulang IL tidak cocok untuk Anda, terapkan CachedDto seperti ini (gunakan konteks tempat metode kelas dasar dipanggil):


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

DeepEquals.Equals dan DeepHash.GetHashCode menggunakan refleksi, itu akan lebih lambat dari Fody, itu tidak fatal untuk aplikasi perusahaan.


Tapi ingat tentang SRP, IQuery seharusnya tidak tahu bahwa itu di-cache.


Solusi terbaik adalah dengan mengimplementasikan IEqualityComparer. Kamus mengambilnya di konstruktor dan menggunakannya saat memasukkan / menghapus / mencari.


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

Sekarang Anda dapat membuang batasan pada TIn, kami mencapai apa yang kami inginkan. Mari kita menulis dekorator caching:


  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)); } 

Perhatikan IConcurrentDictionaryFactory, tujuan dari pabrik ini adalah untuk memberikan contoh kamus, tetapi mengapa tidak membuatnya saja di konstruktor?


Pertama, DI dan SRP, sangat mungkin bahwa Anda perlu menambahkan implementasi pembanding lain (misalnya, lebih mudah untuk jenis DTO tertentu) atau mengubah implementasi sama sekali, dan kedua, situasi mungkin terjadi ketika cache mulai melambat karena refleksi dan abstraksi bocor. Saya akan berkompromi dan jika Persamaan ditimpa di Dto dan GetHashCode tidak akan menggunakan "berat" EqualityComparer.


Tujuan pabrik adalah untuk memeriksa apakah metode diganti, jika demikian, untuk mengembalikan kamus standar menggunakan metode yang didefinisikan ulang dalam DTO, tidak - kamus dengan pembanding.


Pendaftaran


Mari kita kembali ke cara mendaftar semua ini.


Argumen layanan dari metode ConfigureServices adalah kumpulan ServiceDescriptors, setiap deskriptor berisi informasi tentang dependensi terdaftar


 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 } 

Dengan demikian, ServiceDescriptor baru dengan LifeTime = Scoped ditambahkan ke koleksi layanan.
ServiceType = typeof (IService), ImplementType = typeof (Layanan):


 services.AddScoped<IService,Service>(). 

Properti ImplementFactory memungkinkan Anda menentukan cara membuat dependensi, kami akan menggunakannya. Saya akan menulis ekstensi untuk IServiceCollection, yang akan menemukan semua IQuery dan IAsyncQuery di majelis, menggantung dekorator, dan mendaftar.


  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)); } } 

Metode AddDecorator patut mendapat perhatian khusus, di sini kami menggunakan metode statis dari kelas ActivatorUtilities. ActivatorUtilities.CreateInstance menerima IServiceProvider, jenis objek yang akan dibuat dan contoh ketergantungan yang diterima objek ini di konstruktor (Anda dapat menentukan hanya yang tidak terdaftar, sisanya akan diizinkan oleh penyedia)


ActivatorUtilities.GetServiceOrCreateInstance - melakukan hal yang sama, tetapi tidak mengizinkan dependensi yang hilang untuk diteruskan ke konstruktor objek yang dibuat. Jika objek terdaftar dalam wadah, maka itu hanya akan membuatnya (atau mengembalikan yang sudah dibuat), jika tidak, itu akan membuat objek, asalkan dapat menyelesaikan semua ketergantungannya


Dengan demikian, Anda dapat membuat fungsi yang mengembalikan objek cache dan menambahkan deskriptor yang menjelaskan pendaftaran ini ke layanan.


Mari kita menulis tes:


 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 - Mock dari perpustakaan Moq lucu, tetapi untuk menguji berapa kali metode GetSomething () dari repositori telah dipanggil, ia juga menggunakan dekorator, meskipun itu menghasilkan mereka secara otomatis menggunakan Castle.Interceptor. Kami menguji dekorator menggunakan dekorator.

Ini adalah bagaimana Anda dapat menambahkan caching dari semua hasil IQuery <TIn, TOut>, sangat merepotkan untuk menulis begitu banyak kode untuk mengimplementasikan sedikit fungsionalitas.


Solusi lain


Mediatr


Antarmuka perpustakaan pusat:


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

Fungsi utama MediatR adalah untuk menambahkan pembungkus melalui IRequestHandler, misalnya, mengimplementasikan pipa menggunakan antarmuka IPipelineBehavior, ini adalah bagaimana Anda dapat mendaftarkan CachePipelineBehaviour, ini akan diterapkan ke semua antarmuka IRequestHandler yang terdaftar:


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

Kami menerapkan caching 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 dari permintaan, token pembatalan, dan RequestHandlerDelegate datang ke metode Handle. Yang terakhir hanyalah pembungkus panggilan berikutnya dari dekorator dan handler'a lainnya. MediatR memindai mengumpulkan dan mendaftarkan semua implementasi antarmuka itu sendiri. Untuk menggunakan Anda perlu menyuntikkan IMediator, dan memanggil metode Kirim di atasnya lewat Dto:


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

MediatR sendiri akan menemukannya, menemukan implementasi yang sesuai dari IRequestHabdler dan menerapkan semua dekorator (Selain PipelineBehaviour, ada juga IPreRequestHandler dan IPostRequestHandler)


Kastil windsor


Fitur wadah adalah pembuatan pembungkus dinamis, ini adalah AOP dinamis.


Entity Framework menggunakannya untuk Lazy Loading, dalam pengambil properti, metode Load disebut antarmuka ILazyLoader, yang disuntikkan ke dalam kelas semua pembungkus entitas melalui implementasi konstruktor .


Untuk mengkonfigurasi wadah dengan generasi pembungkus, Anda perlu membuat Interceptor dan mendaftarkannya


  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); } } } 

Antarmuka IInvocation memberikan informasi tentang anggota objek yang didekorasi yang diakses, satu-satunya anggota publik antarmuka adalah metode Kueri, jadi kami tidak akan memeriksa apakah akses terdaftar ke sana, tidak ada opsi lain.


Jika ada objek dengan kunci seperti itu di cache, isi nilai balik metode (tanpa memanggilnya), jika tidak, panggil metode Lanjutkan, yang, pada gilirannya, akan memanggil metode yang didekorasi dan mengisi ReturnValue.


Registrasi interseptor dan kode lengkap dapat dilihat di Githab

Source: https://habr.com/ru/post/id449744/


All Articles