.NET'te Önbelleğe Alma ve Bağlantı İşleme: En Boy Odaklı Programlama Eğitimi
Yayınlanan: 2022-03-11Kimse kazan plakası kodunu sevmez. Bunu genellikle ortak nesne yönelimli programlama kalıpları kullanarak azaltırız, ancak genellikle kalıpları kullanmanın kod ek yükü, ilk etapta ortak kod kullanmış olmamızdan daha büyük değilse de neredeyse aynıdır. Kodun belirli davranışları uygulaması gereken bir bölümünü bir şekilde işaretlemek ve uygulamayı başka bir yerde çözmek gerçekten güzel olurdu.
Örneğin, bir StudentRepository
varsa, tüm öğrencileri ilişkisel bir veritabanından almak için Dapper'ı kullanabiliriz:
public class StudentRepository { public Task<IEnumerable<Student>> GetAllAsync(IDbConnection connection) { return connection.GetAllAsync<Student>(); } }
Bu, ilişkisel bir veritabanı deposunun çok basit bir uygulamasıdır. Öğrenci listesi fazla değişmiyorsa ve sık sık aranıyorsa, sistemimizin yanıt süresini optimize etmek için bu öğeleri önbelleğe alabiliriz. Kodumuzda genellikle çok sayıda depomuz olduğundan (ilişkisel olup olmadıklarından bağımsız olarak), bu kesişen önbelleğe alma endişesini bir kenara bırakıp çok kolay bir şekilde kullanmak güzel olurdu, örneğin:
public class StudentRepository { [Cache] public Task<IEnumerable<Student>> GetAllAsync(IDbConnection connection) { return connection.GetAllAsync<Student>(); } }
Bir bonus, veritabanı bağlantıları hakkında endişelenmemek olacaktır. Bu ortak endişeyi de bir kenara bırakın ve harici bağlantı yöneticisini kullanmak için aşağıdaki gibi bir yöntemi etiketlemeniz yeterlidir:
public class StudentRepository { [Cache] [DbConnection] public Task<IEnumerable<Student>> GetAllAsync(IDbConnection connection = null) { return connection.GetAllAsync<Student>(); } }
Bu yazıda, yaygın olarak kullanılan OOP yerine en boy yönelimli kalıp kullanımını ele alacağız. AOP bir süredir var olmasına rağmen, geliştiriciler genellikle AOP yerine OOP'yi tercih ediyor. AOP ile yaptığınız her şey, prosedürel programlamaya karşı OOP gibi, OOP ile de yapılabilirken, AOP geliştiricilere kullanabilecekleri paradigmalarda daha fazla seçenek sunar. AOP kodu farklı şekilde düzenlenmiştir ve bazıları belirli yönlerde (punto amaçlı) OOP'den daha iyi tartışabilir. Sonunda, hangi paradigmanın kullanılacağı kişisel tercihtir.
Nasıl Yaparız
.NET'te AOP kalıpları, daha iyi IL dokuma olarak bilinen ara dil dokuma kullanılarak uygulanabilir. Bu, kod derlemesinden sonra başlatılan bir işlemdir ve bir derleyici tarafından üretilen IL kodunu, kodun beklenen davranışa ulaşmasını sağlamak için değiştirir. Yani, daha önce bahsedilen örneğe bakıldığında, bu sınıfta önbelleğe alma için kod yazmamış olsak da, önbellek kodunu çağırmak için yazdığımız yöntem değiştirilecek (veya değiştirilecektir). Örnekleme için, sonuç şöyle görünmelidir:
// 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?()); } }
Gerekli aletler
Yönler ve entegrasyon testleri de dahil olmak üzere bu makaledeki tüm kodlar, notmarkopadjen/dot-net-aspects-postsharp
GitHub deposunda bulunabilir. IL dokuma için Visual Studio pazarından PostSharp kullanacağız. Ticari bir araçtır ve ticari amaçlar için bir lisans gereklidir. Deneme uğruna, ücretsiz olan PostSharp Essentials lisansını seçebilirsiniz.
Entegrasyon testlerini çalıştırmak istiyorsanız MySQL ve Redis sunucusuna ihtiyacınız olacak. Yukarıdaki kodda, MariaDB 10.4 ve Redis 5.0 kullanarak Docker Compose ile bir laboratuvar yaptım. Kullanmak için Docker'ı yüklemeniz ve Compose yapılandırmasını başlatmanız gerekir:
docker-compose up -d
Elbette diğer sunucuları kullanabilir ve appsettings.json
içindeki bağlantı dizelerini değiştirebilirsiniz.
Temel Görünüme Yönelik Kodlama
AOP'nin engelleme modelini deneyelim. Bunu PostSharp'ta yapmak için yeni bir öznitelik uygulamamız, MethodInterceptionAspect
özniteliğini devralmamız ve gerekli yöntemleri geçersiz kılmamız gerekiyor.
[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); // ... } }
sync ve async çağrıları için iki farklı yöntemimiz olduğunu görüyoruz. .NET zaman uyumsuz özelliklerinden tam olarak yararlanmak için bunları doğru şekilde uygulamak önemlidir. StackExchange.Redis
kitaplığını kullanarak Redis'ten okurken, eşitlenmiş veya zaman uyumsuz kod dalında olmamıza bağlı olarak StringGet
veya StringGetAsync
yöntem çağrılarını kullanırız.
Kod yürütme akışı, MethodInterceptionArgs
, args
nesnesi yöntemlerinin çağrılmasından ve nesnenin özelliklerine değerlerin ayarlanmasından etkilenir. En önemli üyeler:
-
Proceed
(ProceedAsync
) yöntemi - Orijinal yöntemin yürütülmesini çağırır. -
ReturnValue
özelliği - Yöntem çağrısının dönüş değerini içerir. Orijinal yöntemin yürütülmesinden önce boştur ve orijinal dönüş değerini içerdiğinden sonra. Herhangi bir zamanda değiştirilebilir. -
Method
özelliği -System.Reflection.MethodBase
(genellikleSystem.Reflection.MethodInfo
) hedef yöntem yansıma bilgilerini içerir. -
Instance
özelliği - Hedef nesne (yöntem üst örneği). -
Arguments
özelliği - Argüman değerlerini içerir. Herhangi bir zamanda değiştirilebilir.
DbConnection Yönü
Bir IDbConnection
örneği olmadan depo yöntemlerini çağırabilmek ve görünümün bu bağlantıları oluşturmasına ve bunu yöntem çağrısına sağlamasına izin vermek istiyoruz. Bazen, bağlantıyı yine de sağlamak isteyebilirsiniz (örneğin, işlemler nedeniyle) ve bu durumlarda, görünüm hiçbir şey yapmamalıdır.
Aşağıdaki uygulamada, herhangi bir veritabanı varlık deposunda olacağı gibi, yalnızca veritabanı bağlantı yönetimi için kodumuz olacak. Bu özel durumda, bir MySqlConnection
örneği, yöntem yürütmesine ayrıştırılır ve yöntem yürütmesi tamamlandıktan sonra atılır.
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; } } }
Burada önemli olan hususların icra sırasını belirtmektir. Burada en boy rolleri atanarak ve rollerin yürütülmesini sıralayarak yapılmıştır. Herhangi bir şekilde kullanılmayacaksa (örneğin, önbellekten okunan değerler) IDbConnection
oluşturulmasını istemiyoruz. Aşağıdaki niteliklerle tanımlanır:
[ProvideAspectRole(StandardRoles.TransactionHandling)] [AspectRoleDependency(AspectDependencyAction.Order, AspectDependencyPosition.After, StandardRoles.Caching)]
PostSharp ayrıca tüm yönleri sınıf düzeyinde ve derleme düzeyinde uygulayabilir, bu nedenle öznitelik kapsamını tanımlamak önemlidir:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
Bağlantı dizesi appsettings.json
, ancak ConnectionString
statik özelliği kullanılarak geçersiz kılınabilir.
Yürütme akışı aşağıdaki gibidir:
- Aspect, hiçbir değeri olmayan
IDbConnection
isteğe bağlı bağımsız değişken dizinini tanımlar. Bulunmazsa, atlarız. - MySqlConnection, sağlanan
ConnectionString
temel alınarak oluşturulur. -
IDbConnection
bağımsız değişken değeri ayarlanır. - Orijinal yöntem denir.
Dolayısıyla, bu yönü kullanmak istiyorsak, bağlantı sağlanmadan depo yöntemini çağırabiliriz:
await studentRepository.InsertAsync(new Student { Name = "Not Marko Padjen" }, connection: null);
Önbellek Yönü
Burada benzersiz yöntem çağrılarını tanımlamak ve onları önbelleğe almak istiyoruz. Aynı sınıftan aynı yöntem aynı parametrelerle çağrıldıysa, yöntem çağrıları benzersiz olarak kabul edilir.
Aşağıdaki uygulamada, her yöntemde, çağrı için durdurma anahtarı oluşturulur. Bu daha sonra önbellek sunucusunda dönüş değerinin olup olmadığını kontrol etmek için kullanılır. Varsa, orijinal yöntem çağrılmadan döndürülür. Değilse, orijinal yöntem çağrılır ve döndürülen değer daha fazla kullanım için önbellek sunucusuna kaydedilir.
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(", "); } } }
Burada yönlerin sırasına da saygı duyuyoruz. En boy rolü Caching
ve TransactionHandling
sonra gidecek şekilde tanımlanmıştır:

[ProvideAspectRole(StandardRoles.Caching)] [AspectRoleDependency(AspectDependencyAction.Order, AspectDependencyPosition.Before, StandardRoles.TransactionHandling)]
Nitelik kapsamı, DbConnection yönü ile aynıdır:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
Önbelleğe alınmış öğelerin sona erme süresi, ExpirySeconds
(varsayılan 5 dakikadır) genel alanı tanımlanarak her yöntemde ayarlanabilir, örneğin:
[Cache(ExpirySeconds = 2 * 60 /* 2 minutes */)] [DbConnection] public Task<IEnumerable<Student>> GetAllAsync(IDbConnection connection = null) { return connection.GetAllAsync<Student>(); }
Yürütme akışı aşağıdaki gibidir:
- Görünüş, örneğin bu belirli nesne örneğinde önbelleği kullanarak atlamak için bir bayrak sağlayabilen
ICacheAware
olup olmadığını kontrol edin. - Aspect, yöntem çağrısı için bir anahtar oluşturur.
- Aspect Redis bağlantısını açar.
- Oluşturulan anahtarla değer varsa, değer döndürülür ve orijinal yöntem yürütmesi atlanır.
- Değer yoksa, orijinal yöntem çağrılır ve dönüş değeri, oluşturulan bir anahtarla önbelleğe kaydedilir.
Anahtar üretimi için burada bazı kısıtlamalar geçerlidir:
- Bir parametre olarak
IDbConnection
, boş olsun veya olmasın her zaman yoksayılır. Bu, önceki yönün kullanımına uyum sağlamak için bilerek yapılır. - Dize değerleri olarak özel değerler,
<IGNORED>
ve<NULL>
değerleri gibi önbellekten yanlış okumaya neden olabilir. Bu, değer kodlaması ile önlenebilir. - Referans türleri dikkate alınmaz, yalnızca türleri (değer değerlendirmesinde
.ToString()
kullanılır). Çoğu durumda bu iyidir ve ek karmaşıklık eklemez.
Önbelleği düzgün kullanmak için, varlık güncelleme veya varlık silme gibi, önbelleğin süresi dolmadan önce geçersiz kılınması gerekebilir.
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; } }
InvalidateCache
yardımcı yöntemi ifadeyi kabul eder, bu nedenle joker karakterler kullanılabilir (Moq çerçevesine benzer):
this.InvalidateCache(r => r.GetAsync(student.Id, Any<IDbConnection>()));
Bu özellik hiçbir özel parametre olmadan kullanılmaktadır, bu nedenle geliştiriciler yalnızca kod sınırlamalarının farkında olmalıdır.
Hepsini bir araya koy
Bunu denemenin ve hata ayıklamanın en iyi yolu, Paden.Aspects.DAL.Tests
projesinde sağlanan entegrasyon testlerini kullanmaktır.
Aşağıdaki entegrasyon test yöntemi, gerçek sunucuları (ilişkisel veritabanı ve önbellek) kullanır. Bağlantı cephesi sadece metot çağrılarını takip etmek için kullanılır.
[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); }
Veritabanı, sınıf fikstürü kullanılarak otomatik olarak oluşturulur ve atılır:
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(); } } }
Hata ayıklama sırasında manuel denetim gerçekleştirilebilir, çünkü testler yürütüldükten sonra veritabanı silinir ve önbellek manuel olarak geçersiz kılınır.
Örneğin Get_Should_Call_Database_If_Entity_Not_Dirty_Otherwise_Read_From_Cache
testinin yürütülmesi sırasında Redis veritabanında aşağıdaki değerleri bulabiliriz:
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\"}"
Entegrasyon testi GetAllAsync_Should_Not_Call_Database_On_Second_Call
ayrıca önbelleğe alınan çağrıların orijinal veri kaynağı çağrılarından daha performanslı olmasını sağlıyor. Ayrıca, her bir çağrıyı yürütmenin ne kadar zaman aldığını bize bildiren iz üretirler:
Database run time (ms): 73 Cache run time (ms): 9
Üretimde Kullanmadan Önce İyileştirmeler
Burada verilen kod eğitim amaçlı yapılmıştır. Gerçek dünya sisteminde kullanmadan önce bazı iyileştirmeler yapılabilir:
- DbConnection yönü:
- Gerekirse bağlantı havuzu uygulanabilir.
- Birden çok bağlantı dizesi uygulanabilir. Bunun için yaygın kullanım, salt okunur ve okuma-yazma bağlantı türlerini ayırt ettiğimiz ilişkisel veritabanı kümesidir.
- Önbellek yönü:
- Gerekirse bağlantı havuzu uygulanabilir.
- Referans tipi değerleri, kullanım durumuna bağlı olarak, oluşturulan anahtarın bir parçası olarak da düşünülebilir. Çoğu durumda, muhtemelen yalnızca performans dezavantajı sağlarlar.
Bu özellikler, kullanıldıkları sistemin özel gereksinimleri ile ilgili oldukları için burada uygulanmamıştır ve düzgün uygulanmadığı takdirde sistem performansına katkıda bulunmayacaktır.
Çözüm
SOLID ilkeleri ilkesinden “tek sorumluluk”, “açık-kapalı” ve “bağımlılığın tersine çevrilmesi”nin OOP ile olduğundan daha iyi AOP ile uygulanabileceği iddia edilebilir. Gerçek şu ki, .NET geliştiricilerinin hedefi, belirli durumlara uygulanabilen birçok araç, çerçeve ve kalıpla elde edilebilecek iyi bir kod organizasyonu olmalıdır.
Yinelemek gerekirse: Yönler ve entegrasyon testleri de dahil olmak üzere bu makaledeki tüm kodlar notmarkopadjen/dot-net-aspects-postsharp
GitHub deposunda bulunabilir. IL dokuma için Visual Studio pazarından PostSharp kullandık. Kod, MariaDB 10.4 ve Redis 5.0 kullanılarak docker oluşturma ile yapılmış bir laboratuvarı içerir.