Caching dan Penanganan Koneksi di .NET: Tutorial Pemrograman Berorientasi Aspek

Diterbitkan: 2022-03-11

Tidak ada yang suka kode boilerplate. Kami biasanya menguranginya dengan menggunakan pola pemrograman berorientasi objek yang umum, tetapi sering kali kode overhead menggunakan pola hampir sama—jika tidak lebih besar—daripada jika kami menggunakan kode boilerplate di tempat pertama. Akan sangat menyenangkan untuk menandai bagian dari kode yang harus mengimplementasikan perilaku tertentu, dan menyelesaikan implementasi di tempat lain.

Misalnya, jika kita memiliki StudentRepository , kita dapat menggunakan Dapper untuk mendapatkan semua siswa dari database relasional:

 public class StudentRepository { public Task<IEnumerable<Student>> GetAllAsync(IDbConnection connection) { return connection.GetAllAsync<Student>(); } }

Ini adalah implementasi yang sangat sederhana dari repositori database relasional. Jika daftar siswa tidak banyak berubah dan sering dipanggil, kami dapat men-cache item tersebut untuk mengoptimalkan waktu respons sistem kami. Karena kami biasanya memiliki banyak repositori (terlepas dari apakah mereka relasional atau tidak) dalam kode kami, alangkah baiknya untuk mengesampingkan masalah caching ini dan menggunakannya dengan sangat mudah, seperti:

 public class StudentRepository { [Cache] public Task<IEnumerable<Student>> GetAllAsync(IDbConnection connection) { return connection.GetAllAsync<Student>(); } }

Bonusnya adalah tidak perlu khawatir tentang koneksi database. Singkirkan masalah lintas sektoral ini juga, dan cukup beri label metode untuk menggunakan manajer koneksi eksternal, seperti:

 public class StudentRepository { [Cache] [DbConnection] public Task<IEnumerable<Student>> GetAllAsync(IDbConnection connection = null) { return connection.GetAllAsync<Student>(); } }

Dalam artikel ini, kami akan mempertimbangkan penggunaan pola berorientasi aspek alih-alih OOP yang umum digunakan. Meskipun AOP telah ada untuk beberapa waktu sekarang, pengembang biasanya lebih memilih OOP daripada AOP. Meskipun semua yang Anda lakukan dengan AOP dapat dilakukan dengan OOP juga, seperti pemrograman prosedural vs. OOP, AOP memberi pengembang lebih banyak pilihan dalam paradigma yang dapat mereka gunakan. Kode AOP diatur secara berbeda, dan beberapa mungkin berpendapat lebih baik, pada aspek tertentu (pun intended) daripada OOP. Pada akhirnya, pilihan paradigma mana yang akan digunakan adalah preferensi pribadi.

Bagaimana kita melakukannya

Dalam .NET, pola AOP dapat diimplementasikan menggunakan bahasa perantara tenun, lebih dikenal sebagai tenun IL . Ini adalah proses yang dimulai setelah kompilasi kode, dan ini mengubah kode IL yang dihasilkan oleh kompiler, untuk membuat kode mencapai perilaku yang diharapkan. Jadi, melihat contoh yang telah disebutkan, meskipun kami tidak menulis kode untuk caching di kelas ini, metode yang kami tulis akan diubah (atau diganti) untuk memanggil kode caching. Demi ilustrasi, hasil akhirnya akan terlihat seperti ini:

 // Weaved by PostSharp public class StudentRepository { [DebuggerTargetMethod(100663306)] [DebuggerBindingMethod(100663329)] [DebuggerBindingMethod(100663335)] public async Task<IEnumerable<Student>> GetAllAsync( IDbConnection connection = null) { AsyncMethodInterceptionArgsImpl<IEnumerable<Student>> interceptionArgsImpl; try { // ISSUE: reference to a compiler-generated field await <>z__a_1.a2.OnInvokeAsync((MethodInterceptionArgs) interceptionArgsImpl); // ISSUE: reference to a compiler-generated field this.<>1__state = -2; } finally { } return (IEnumerable<Student>) interceptionArgsImpl.TypedReturnValue; } [DebuggerSourceMethod(100663300)] private Task<IEnumerable<Student>> <GetAllAsync>z__OriginalMethod( [Optional] IDbConnection connection) { return (Task<IEnumerable<Student>>) SqlMapperExtensions.GetAllAsync<Student>(connection, (IDbTransaction) null, new int?()); } }

Alat yang Diperlukan

Semua kode dari artikel ini, termasuk pengujian aspek dan integrasi, dapat ditemukan di repositori GitHub notmarkopadjen/dot-net-aspects-postsharp . Untuk tenun IL, kami akan menggunakan PostSharp dari pasar Visual Studio. Ini adalah alat komersial, dan lisensi diperlukan untuk tujuan komersial. Demi bereksperimen, Anda dapat memilih lisensi PostSharp Essentials, yang gratis.

Jika Anda ingin menjalankan tes integrasi, Anda memerlukan server MySQL dan Redis. Pada kode di atas, saya telah membuat lab dengan Docker Compose menggunakan MariaDB 10.4 dan Redis 5.0. Untuk menggunakannya, Anda perlu menginstal Docker dan mem-boot konfigurasi Compose:

 docker-compose up -d

Anda tentu saja dapat menggunakan server lain dan mengubah string koneksi di appsettings.json .

Pengodean Berorientasi Aspek Dasar

Mari kita coba pola intersepsi AOP. Untuk melakukan ini di PostSharp, kita perlu mengimplementasikan atribut baru, mewarisi atribut MethodInterceptionAspect dan mengganti metode yang diperlukan.

 [PSerializable] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public class CacheAttribute : MethodInterceptionAspect { // ... public override void OnInvoke(MethodInterceptionArgs args) { // ... var redisValue = db.StringGet(key); // ... } public override async Task OnInvokeAsync(MethodInterceptionArgs args) { // ... var redisValue = await db.StringGetAsync(key); // ... } }

Kami melihat bahwa kami memiliki dua metode berbeda untuk panggilan sinkronisasi dan asinkron. Penting untuk menerapkannya dengan benar untuk memanfaatkan sepenuhnya fitur .NET async. Saat membaca dari Redis menggunakan pustaka StackExchange.Redis , kami menggunakan pemanggilan metode StringGet atau StringGetAsync , bergantung pada apakah kami berada di cabang kode sinkron atau asinkron.

Alur eksekusi kode dipengaruhi oleh metode pemanggilan MethodInterceptionArgs , objek args , dan pengaturan nilai ke properti objek. Anggota terpenting:

  • Metode Proceed ( ProceedAsync ) - Memanggil eksekusi metode asli.
  • Properti ReturnValue - Berisi nilai kembalian dari pemanggilan metode. Sebelum eksekusi metode asli, itu kosong, dan setelah itu berisi nilai pengembalian asli. Itu bisa diganti kapan saja.
  • Properti Method - System.Reflection.MethodBase (biasanya System.Reflection.MethodInfo ) berisi informasi refleksi metode target.
  • Properti Instance - Objek target (contoh metode induk).
  • Properti Arguments - Berisi nilai argumen. Itu bisa diganti kapan saja.

Aspek Koneksi Db

Kami ingin dapat memanggil metode repositori tanpa instance IDbConnection , dan membiarkan aspek tersebut membuat koneksi tersebut dan menyediakannya ke pemanggilan metode. Terkadang, Anda mungkin ingin tetap menyediakan koneksi (misalnya, karena transaksi) dan, pada kesempatan itu, aspek tersebut tidak akan melakukan apa pun.

Dalam implementasi di bawah ini, kita hanya akan memiliki kode untuk manajemen koneksi database, seperti yang kita miliki di repositori entitas database mana pun. Dalam kasus khusus ini, sebuah instance dari MySqlConnection diuraikan ke eksekusi metode dan dibuang setelah eksekusi metode selesai.

 using Microsoft.Extensions.Configuration; using MySql.Data.MySqlClient; using PostSharp.Aspects; using PostSharp.Aspects.Dependencies; using PostSharp.Serialization; using System; using System.Data; using System.Threading.Tasks; namespace Paden.Aspects.Storage.MySQL { [PSerializable] [ProvideAspectRole(StandardRoles.TransactionHandling)] [AspectRoleDependency(AspectDependencyAction.Order, AspectDependencyPosition.After, StandardRoles.Caching)] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public class DbConnectionAttribute : MethodInterceptionAspect { const string DefaultConnectionStringName = "DefaultConnection"; static Lazy<IConfigurationRoot> config; static string connectionString; public static string ConnectionString { get { return connectionString ?? config.Value.GetConnectionString(DefaultConnectionStringName); } set { connectionString = value; } } static DbConnectionAttribute() { config = new Lazy<IConfigurationRoot>(() => new ConfigurationBuilder().AddJsonFile("appsettings.json", false, false).Build()); } public override void OnInvoke(MethodInterceptionArgs args) { var i = GetArgumentIndex(args); if (!i.HasValue) { args.Proceed(); return; } using (IDbConnection db = new MySqlConnection(ConnectionString)) { args.Arguments.SetArgument(i.Value, db); args.Proceed(); } } public override async Task OnInvokeAsync(MethodInterceptionArgs args) { var i = GetArgumentIndex(args); if (!i.HasValue) { await args.ProceedAsync(); return; } using (IDbConnection db = new MySqlConnection(ConnectionString)) { args.Arguments.SetArgument(i.Value, db); await args.ProceedAsync(); } } private int? GetArgumentIndex(MethodInterceptionArgs args) { var parameters = args.Method.GetParameters(); for (int i = 0; i < parameters.Length; i++) { var parameter = parameters[i]; if (parameter.ParameterType == typeof(IDbConnection) && parameter.IsOptional && args.Arguments[i] == null) { return i; } } return null; } } }

Penting di sini adalah untuk menentukan urutan pelaksanaan aspek. Di sini, telah dilakukan dengan menetapkan peran aspek, dan memerintahkan eksekusi peran. Kami tidak ingin IDbConnection dibuat jika tidak akan digunakan (misalnya, nilai yang dibaca dari cache). Ini ditentukan oleh atribut berikut:

 [ProvideAspectRole(StandardRoles.TransactionHandling)] [AspectRoleDependency(AspectDependencyAction.Order, AspectDependencyPosition.After, StandardRoles.Caching)]

PostSharp juga dapat mengimplementasikan semua aspek pada tingkat kelas, dan tingkat perakitan, jadi penting untuk menentukan cakupan atribut:

 [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]

String koneksi sedang dibaca dari appsettings.json , tetapi dapat diganti menggunakan properti static ConnectionString .

Alur eksekusinya sebagai berikut:

  1. Aspect mengidentifikasi indeks argumen opsional IDbConnection yang tidak memiliki nilai yang diberikan. Jika tidak ditemukan, kita lewati.
  2. MySqlConnection dibuat berdasarkan ConnectionString yang disediakan.
  3. Nilai argumen IDbConnection disetel.
  4. Metode asli disebut.

Jadi, jika kita ingin menggunakan aspek ini, kita bisa memanggil metode repositori tanpa koneksi yang disediakan:

 await studentRepository.InsertAsync(new Student { Name = "Not Marko Padjen" }, connection: null);

Aspek Cache

Di sini kami ingin mengidentifikasi panggilan metode unik dan menyimpannya dalam cache. Pemanggilan metode dianggap unik jika metode yang sama dari kelas yang sama telah dipanggil dengan parameter yang sama.

Dalam implementasi di bawah ini, pada setiap metode, kunci intersepsi dibuat untuk panggilan. Ini kemudian digunakan untuk memeriksa apakah nilai kembalian ada di server cache. Jika ya, itu dikembalikan tanpa memanggil metode asli. Jika tidak, metode asli dipanggil, dan nilai yang dikembalikan disimpan ke server cache untuk penggunaan lebih lanjut.

 using Microsoft.Extensions.Configuration; using Newtonsoft.Json; using PostSharp.Aspects; using PostSharp.Aspects.Dependencies; using PostSharp.Serialization; using StackExchange.Redis; using System; using System.Collections.Generic; using System.Data; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Text; using System.Threading.Tasks; namespace Paden.Aspects.Caching.Redis { [PSerializable] [ProvideAspectRole(StandardRoles.Caching)] [AspectRoleDependency(AspectDependencyAction.Order, AspectDependencyPosition.Before, StandardRoles.TransactionHandling)] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public class CacheAttribute : MethodInterceptionAspect { const int DefaultExpirySeconds = 5 * 60; static Lazy<string> redisServer; public int ExpirySeconds = DefaultExpirySeconds; private TimeSpan? Expiry => ExpirySeconds == -1 ? (TimeSpan?)null : TimeSpan.FromSeconds(ExpirySeconds); static CacheAttribute() { redisServer = new Lazy<string>(() => new ConfigurationBuilder().AddJsonFile("appsettings.json", false, false).Build()["Redis:Server"]); } public override void OnInvoke(MethodInterceptionArgs args) { if (args.Instance is ICacheAware cacheAware && !cacheAware.CacheEnabled) { args.Proceed(); return; } var key = GetKey(args.Method as MethodInfo, args.Arguments); using (var connection = ConnectionMultiplexer.Connect(redisServer.Value)) { var db = connection.GetDatabase(); var redisValue = db.StringGet(key); if (redisValue.IsNullOrEmpty) { args.Proceed(); db.StringSet(key, JsonConvert.SerializeObject(args.ReturnValue), Expiry); } else { args.ReturnValue = JsonConvert.DeserializeObject(redisValue.ToString(), (args.Method as MethodInfo).ReturnType); } } } public override async Task OnInvokeAsync(MethodInterceptionArgs args) { if (args.Instance is ICacheAware cacheAware && !cacheAware.CacheEnabled) { await args.ProceedAsync(); return; } var key = GetKey(args.Method as MethodInfo, args.Arguments); using (var connection = ConnectionMultiplexer.Connect(redisServer.Value)) { var db = connection.GetDatabase(); var redisValue = await db.StringGetAsync(key); if (redisValue.IsNullOrEmpty) { await args.ProceedAsync(); db.StringSet(key, JsonConvert.SerializeObject(args.ReturnValue), Expiry); } else { args.ReturnValue = JsonConvert.DeserializeObject(redisValue.ToString(), (args.Method as MethodInfo).ReturnType.GenericTypeArguments[0]); } } } private string GetKey(MethodInfo method, IList<object> values) { var parameters = method.GetParameters(); var keyBuilder = GetKeyBuilder(method); keyBuilder.Append("("); foreach (var parameter in parameters) { AppendParameterValue(keyBuilder, parameter, values[parameter.Position]); } if (parameters.Any()) { keyBuilder.Remove(keyBuilder.Length - 2, 2); } keyBuilder.Append(")"); return keyBuilder.ToString(); } public static void InvalidateCache<T, TResult>(Expression<Func<T, TResult>> expression) { var methodCallExpression = expression.Body as MethodCallExpression; var keyBuilder = GetKeyBuilder(methodCallExpression.Method); var parameters = methodCallExpression.Method.GetParameters(); var anyMethod = typeof(CacheExtensions).GetMethod(nameof(CacheExtensions.Any)); keyBuilder.Append("("); for (int i = 0; i < parameters.Length; i++) { var parameter = parameters[i]; var argument = methodCallExpression.Arguments[i]; object value = null; if (argument is ConstantExpression constantArgument) { value = constantArgument.Value; } else if (argument is MemberExpression memberArgument) { value = Expression.Lambda(memberArgument).Compile().DynamicInvoke(); } else if (argument is MethodCallExpression methodCallArgument) { if (methodCallArgument.Method == anyMethod.MakeGenericMethod(methodCallArgument.Method.GetGenericArguments())) { value = "*"; } } AppendParameterValue(keyBuilder, parameter, value); } if (methodCallExpression.Arguments.Any()) { keyBuilder.Remove(keyBuilder.Length - 2, 2); } keyBuilder.Append(")"); using (var connection = ConnectionMultiplexer.Connect(redisServer.Value)) { connection.GetDatabase().ScriptEvaluate(@" local keys = redis.call('keys', ARGV[1]) for i=1, #keys, 5000 do redis.call('del', unpack(keys, i, math.min(i + 4999, #keys))) end", values: new RedisValue[] { CacheExtensions.EscapeRedisString(keyBuilder.ToString()) }); } } private static StringBuilder GetKeyBuilder(MethodInfo method) { var keyBuilder = new StringBuilder(); keyBuilder.Append(method.ReturnType.FullName); keyBuilder.Append(" {"); keyBuilder.Append(method.ReflectedType.AssemblyQualifiedName); keyBuilder.Append("}."); keyBuilder.Append(method.ReflectedType.FullName); keyBuilder.Append("."); keyBuilder.Append(method.Name); return keyBuilder; } private static void AppendParameterValue(StringBuilder keyBuilder, ParameterInfo parameter, object value) { keyBuilder.Append(parameter.ParameterType.FullName); keyBuilder.Append(" "); if (parameter.ParameterType == typeof(IDbConnection)) { keyBuilder.Append("<IGNORED>"); } else { keyBuilder.Append(value == null ? "<NULL>" : value.ToString()); } keyBuilder.Append(", "); } } }

Di sini, kami juga menghormati urutan aspek. Peran aspeknya adalah Caching , dan didefinisikan untuk mengikuti TransactionHandling :

 [ProvideAspectRole(StandardRoles.Caching)] [AspectRoleDependency(AspectDependencyAction.Order, AspectDependencyPosition.Before, StandardRoles.TransactionHandling)]

Cakupan atribut sama dengan aspek DbConnection:

 [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]

Kedaluwarsa item yang di-cache dapat diatur pada setiap metode dengan mendefinisikan bidang publik ExpirySeconds (default adalah 5 menit) misalnya:

 [Cache(ExpirySeconds = 2 * 60 /* 2 minutes */)] [DbConnection] public Task<IEnumerable<Student>> GetAllAsync(IDbConnection connection = null) { return connection.GetAllAsync<Student>(); }

Alur eksekusinya sebagai berikut:

  1. Aspek memeriksa apakah instance adalah ICacheAware yang dapat memberikan tanda untuk dilewati menggunakan cache pada instance objek tertentu ini.
  2. Aspect menghasilkan kunci untuk pemanggilan metode.
  3. Aspect membuka koneksi Redis.
  4. Jika ada nilai dengan kunci yang dihasilkan, nilai dikembalikan dan eksekusi metode asli dilewati.
  5. Jika nilai tidak ada, metode asli dipanggil dan nilai yang dikembalikan disimpan dalam cache dengan kunci yang dibuat.

Untuk pembuatan kunci, beberapa batasan di sini berlaku:

  1. IDbConnection sebagai parameter selalu diabaikan, menjadi nol atau tidak. Hal ini dilakukan dengan sengaja untuk mengakomodasi penggunaan aspek sebelumnya.
  2. Nilai khusus sebagai nilai string dapat menyebabkan kesalahan pembacaan dari cache, seperti nilai <IGNORED> dan <NULL> . Ini dapat dihindari dengan pengkodean nilai.
  3. Jenis referensi tidak dipertimbangkan, hanya jenisnya ( .ToString() digunakan pada evaluasi nilai). Untuk sebagian besar kasus, ini baik-baik saja, dan tidak menambah kerumitan tambahan.

Untuk menggunakan cache dengan benar, mungkin diperlukan untuk membatalkan cache sebelum kedaluwarsa, seperti pada pembaruan entitas, atau penghapusan entitas.

 public class StudentRepository : ICacheAware { // ... [Cache] [DbConnection] public Task<IEnumerable<Student>> GetAllAsync(IDbConnection connection = null) { return connection.GetAllAsync<Student>(); } [Cache] [DbConnection] public Task<Student> GetAsync(int id, IDbConnection connection = null) { return connection.GetAsync<Student>(id); } [DbConnection] public async Task<int> InsertAsync(Student student, IDbConnection connection = null) { var result = await connection.InsertAsync(student); this.InvalidateCache(r => r.GetAllAsync(Any<IDbConnection>())); return result; } [DbConnection] public async Task<bool> UpdateAsync(Student student, IDbConnection connection = null) { var result = await connection.UpdateAsync(student); this.InvalidateCache(r => r.GetAllAsync(Any<IDbConnection>())); this.InvalidateCache(r => r.GetAsync(student.Id, Any<IDbConnection>())); return result; } [DbConnection] public async Task<bool> DeleteAsync(Student student, IDbConnection connection = null) { var result = await connection.DeleteAsync(student); this.InvalidateCache(r => r.GetAllAsync(Any<IDbConnection>())); this.InvalidateCache(r => r.GetAsync(student.Id, Any<IDbConnection>())); return result; } }

Metode pembantu InvalidateCache menerima ekspresi, sehingga wildcard dapat digunakan (mirip seperti kerangka kerja Moq):

 this.InvalidateCache(r => r.GetAsync(student.Id, Any<IDbConnection>()));

Aspek ini digunakan tanpa parameter khusus, jadi pengembang seharusnya hanya mengetahui batasan kode.

Menyatukan Semuanya

Cara terbaik adalah dengan mencobanya dan debug adalah dengan menggunakan tes integrasi yang disediakan dalam proyek Paden.Aspects.DAL.Tests .

Metode pengujian integrasi berikut menggunakan server nyata (database relasional dan cache). Fasad koneksi hanya digunakan untuk melacak pemanggilan metode.

 [Fact] public async Task Get_Should_Call_Database_If_Entity_Not_Dirty_Otherwise_Read_From_Cache() { var student = new Student { Id = studentId, Name = "Not Marko Padjen" }; var studentUpdated = new Student { Id = studentId, Name = "Not Marko Padjen UPDATED" }; await systemUnderTest.InsertAsync(student); // Gets entity by id, should save in cache Assert.Equal(student.Name, (await systemUnderTest.GetAsync(studentId)).Name); // Updates entity by id, should invalidate cache await systemUnderTest.UpdateAsync(studentUpdated); var connectionMock = fixture.GetConnectionFacade(); // Gets entity by id, ensures that it is the expected one Assert.Equal(studentUpdated.Name, (await systemUnderTest.GetAsync(studentId, connectionMock)).Name); // Ensures that database was used for the call Mock.Get(connectionMock).Verify(m => m.CreateCommand(), Times.Once); var connectionMockUnused = fixture.GetConnectionFacade(); // Calls again, should read from cache Assert.Equal(studentUpdated.Name, (await systemUnderTest.GetAsync(studentId, connectionMockUnused)).Name); // Ensures that database was not used Mock.Get(connectionMockUnused).Verify(m => m.CreateCommand(), Times.Never); }

Basis data secara otomatis dibuat dan dibuang menggunakan perlengkapan kelas:

 using Microsoft.Extensions.Configuration; using Moq; using MySql.Data.MySqlClient; using Paden.Aspects.DAL.Entities; using Paden.Aspects.Storage.MySQL; using System; using System.Data; namespace Paden.Aspects.DAL.Tests { public class DatabaseFixture : IDisposable { public MySqlConnection Connection { get; private set; } public readonly string DatabaseName = $"integration_test_{Guid.NewGuid():N}"; public DatabaseFixture() { var config = new ConfigurationBuilder().AddJsonFile("appsettings.json", false, false).Build(); var connectionString = config.GetConnectionString("DefaultConnection"); Connection = new MySqlConnection(connectionString); Connection.Open(); new MySqlCommand($"CREATE DATABASE `{DatabaseName}`;", Connection).ExecuteNonQuery(); Connection.ChangeDatabase(DatabaseName); DbConnectionAttribute.ConnectionString = $"{connectionString};Database={DatabaseName}"; } public void RecreateTables() { new MySqlCommand(Student.ReCreateStatement, Connection).ExecuteNonQuery(); } public IDbConnection GetConnectionFacade() { var connectionMock = Mock.Of<IDbConnection>(); Mock.Get(connectionMock).Setup(m => m.CreateCommand()).Returns(Connection.CreateCommand()).Verifiable(); Mock.Get(connectionMock).SetupGet(m => m.State).Returns(ConnectionState.Open).Verifiable(); return connectionMock; } public void Dispose() { try { new MySqlCommand($"DROP DATABASE IF EXISTS `{DatabaseName}`;", Connection).ExecuteNonQuery(); } catch (Exception) { // ignored } Connection.Close(); } } }

Pemeriksaan manual dapat dilakukan selama debugging hanya karena setelah pengujian dijalankan, database dihapus dan cache secara manual tidak valid.

Misalnya, selama pelaksanaan pengujian Get_Should_Call_Database_If_Entity_Not_Dirty_Otherwise_Read_From_Cache , kita dapat menemukan nilai berikut dalam database Redis:

 127.0.0.1:6379> KEYS * 1) "System.Threading.Tasks.Task`1[[Paden.Aspects.DAL.Entities.Student, Paden.Aspects.DAL, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]] {Paden.Aspects.DAL.StudentRepository, Paden.Aspects.DAL, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null}.Paden.Aspects.DAL.StudentRepository.GetAsync(System.Int32 1, System.Data.IDbConnection <IGNORED>)" 127.0.0.1:6379> GET "System.Threading.Tasks.Task`1[[Paden.Aspects.DAL.Entities.Student, Paden.Aspects.DAL, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]] {Paden.Aspects.DAL.StudentRepository, Paden.Aspects.DAL, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null}.Paden.Aspects.DAL.StudentRepository.GetAsync(System.Int32 1, System.Data.IDbConnection <IGNORED>)" "{\"Id\":1,\"Name\":\"Not Marko Padjen\"}"

Tes integrasi GetAllAsync_Should_Not_Call_Database_On_Second_Call juga memastikan bahwa panggilan yang di-cache lebih berkinerja daripada panggilan sumber data asli. Mereka juga menghasilkan jejak yang memberi tahu kita berapa banyak waktu yang dibutuhkan untuk mengeksekusi setiap panggilan:

 Database run time (ms): 73 Cache run time (ms): 9

Perbaikan Sebelum Menggunakan dalam Produksi

Kode yang disediakan di sini dibuat untuk tujuan pendidikan. Sebelum menggunakannya dalam sistem dunia nyata, beberapa perbaikan mungkin dilakukan:

  • Aspek DbConnection:
    • Kumpulan koneksi dapat diimplementasikan jika diperlukan.
    • Beberapa string koneksi dapat diimplementasikan. Penggunaan umum untuk ini adalah cluster database relasional di mana kita membedakan tipe koneksi read-only dan read-write.
  • Aspek cache:
    • Kumpulan koneksi dapat diimplementasikan jika diperlukan.
    • Nilai tipe referensi juga dapat dianggap sebagai bagian dari kunci yang dihasilkan, tergantung pada kasus penggunaan. Dalam kebanyakan kasus, mereka mungkin hanya akan memberikan kelemahan kinerja.

Fitur-fitur ini belum diimplementasikan di sini karena terkait dengan persyaratan khusus dari sistem yang digunakan, dan jika tidak diterapkan dengan benar tidak akan berkontribusi pada kinerja sistem.

Kesimpulan

Orang mungkin berpendapat bahwa "tanggung jawab tunggal," "terbuka-tertutup," dan "inversi ketergantungan" dari prinsip prinsip SOLID mungkin lebih baik diimplementasikan dengan AOP daripada dengan OOP. Faktanya adalah bahwa tujuan untuk pengembang .NET harus menjadi organisasi kode yang baik, yang dapat dicapai dengan banyak alat, kerangka kerja, dan pola yang berlaku untuk situasi tertentu.

Sekadar menegaskan: Semua kode dari artikel ini, termasuk aspek dan tes integrasi, dapat ditemukan di repositori GitHub notmarkopadjen/dot-net-aspects-postsharp . Untuk tenun IL, kami menggunakan PostSharp dari pasar Visual Studio. Kode tersebut mencakup lab yang dibuat dengan komposisi buruh pelabuhan menggunakan MariaDB 10.4 dan Redis 5.0.