Memorarea în cache și gestionarea conexiunilor în .NET: Un tutorial de programare orientat pe aspecte
Publicat: 2022-03-11Nimănui nu-i place codul standard. De obicei, o reducem folosind modele obișnuite de programare orientate pe obiecte, dar de multe ori suprafața de cod pentru utilizarea modelelor este aproape aceeași – dacă nu mai mare – decât dacă am fi folosit codul standard în primul rând. Ar fi foarte frumos să marchezi cumva o parte din cod care ar trebui să implementeze un anumit comportament și să rezolvi implementarea în altă parte.
De exemplu, dacă avem un StudentRepository
, putem folosi Dapper pentru a obține toți studenții dintr-o bază de date relațională:
public class StudentRepository { public Task<IEnumerable<Student>> GetAllAsync(IDbConnection connection) { return connection.GetAllAsync<Student>(); } }
Aceasta este o implementare foarte simplă a unui depozit de baze de date relaționale. Dacă lista de studenți nu se schimbă prea mult și este apelată des, putem stoca acele elemente în cache pentru a optimiza timpul de răspuns al sistemului nostru. Deoarece avem de obicei o mulțime de depozite (indiferent dacă sunt relaționale sau nu) în codul nostru, ar fi bine să lăsăm deoparte această preocupare transversală a stocării în cache și să o folosim foarte ușor, cum ar fi:
public class StudentRepository { [Cache] public Task<IEnumerable<Student>> GetAllAsync(IDbConnection connection) { return connection.GetAllAsync<Student>(); } }
Un bonus ar fi să nu vă faceți griji cu privire la conexiunile la baze de date. Lăsați și această preocupare transversală deoparte și etichetați doar o metodă de a utiliza managerul de conexiuni extern, cum ar fi:
public class StudentRepository { [Cache] [DbConnection] public Task<IEnumerable<Student>> GetAllAsync(IDbConnection connection = null) { return connection.GetAllAsync<Student>(); } }
În acest articol, vom lua în considerare utilizarea modelelor orientate pe aspect în loc de OOP utilizat în mod obișnuit. Deși AOP există de ceva timp, dezvoltatorii preferă, de obicei, OOP decât AOP. În timp ce tot ceea ce faci cu AOP poate fi făcut și cu OOP, cum ar fi programarea procedurală vs. OOP, AOP oferă dezvoltatorilor mai multe opțiuni în paradigmele pe care le pot folosi. Codul AOP este organizat diferit, iar unii ar putea argumenta mai bine asupra anumitor aspecte (jocuri de cuvinte) decât OOP. În cele din urmă, alegerea paradigmei de utilizat este preferința personală.
Cum facem
În .NET, modelele AOP pot fi implementate folosind țeserea limbajului intermediar, mai bine cunoscut sub numele de țesere IL . Acesta este un proces care este inițiat după compilarea codului și modifică codul IL produs de un compilator, pentru ca codul să obțină comportamentul așteptat. Deci, uitându-ne la exemplul deja menționat, deși nu am scris cod pentru stocarea în cache în această clasă, metoda pe care am scris-o va fi schimbată (sau înlocuită) pentru a apela codul de cache. De dragul ilustrației, rezultatul final ar trebui să arate cam așa:
// 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?()); } }
Instrumente necesare
Tot codul din acest articol, inclusiv aspectele și testele de integrare, poate fi găsit în depozitul GitHub notmarkopadjen/dot-net-aspects-postsharp
. Pentru țesutul IL, vom folosi PostSharp de pe piața Visual Studio. Este un instrument comercial și este necesară o licență în scopuri comerciale. De dragul experimentului, puteți selecta licența PostSharp Essentials, care este gratuită.
Dacă doriți să rulați testele de integrare, veți avea nevoie de serverul MySQL și Redis. În codul de mai sus, am făcut un laborator cu Docker Compose folosind MariaDB 10.4 și Redis 5.0. Pentru a-l folosi, va trebui să instalați Docker și să porniți configurația Compose:
docker-compose up -d
Puteți, desigur, să utilizați alte servere și să modificați șirurile de conexiune în appsettings.json
.
Codare de bază orientată pe aspecte
Să încercăm modelul de interceptare al AOP. Pentru a face acest lucru în PostSharp, trebuie să implementăm un nou atribut, să moștenim atributul MethodInterceptionAspect
și să înlocuim metodele necesare.
[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); // ... } }
Vedem că avem două metode diferite pentru apelurile sincronizate și asincrone. Este important să le implementați corect pentru a profita din plin de caracteristicile asincrone .NET. Când citim din Redis folosind biblioteca StackExchange.Redis
, folosim apeluri de metodă StringGet
sau StringGetAsync
, în funcție de dacă suntem în ramura de cod sincronizat sau asincron.
Fluxul de execuție a codului este afectat de invocarea metodelor MethodInterceptionArgs
, obiectul args
și setarea valorilor proprietăților obiectului. Cei mai importanti membri:
- Metoda
Proceed
(ProceedAsync
) - Invocă execuția metodei originale. - Proprietate
ReturnValue
- Conține valoarea returnată a apelului de metodă. Înainte de execuția inițială a metodei, aceasta este goală, iar după aceasta conține valoarea returnată inițială. Poate fi inlocuit oricand. - Proprietatea
Method
-System.Reflection.MethodBase
(de obiceiSystem.Reflection.MethodInfo
) conține informații despre reflecția metodei țintă. - Proprietatea
Instance
- Obiect țintă (instanță părinte a metodei). - Proprietatea
Arguments
- Conține valorile argumentelor. Poate fi inlocuit oricand.
Aspectul DbConnection
Dorim să putem apela metode de depozit fără o instanță de IDbConnection
și să lăsăm aspectul să creeze acele conexiuni și să le furnizeze apelului de metodă. Uneori, poate doriți să furnizați oricum conexiunea (de exemplu, din cauza tranzacțiilor) și, în acele ocazii, aspectul nu ar trebui să facă nimic.
În implementarea de mai jos, vom avea cod doar pentru gestionarea conexiunii la baza de date, așa cum am avea în orice depozit de entități de bază de date. În acest caz particular, o instanță a MySqlConnection
este analizată la execuția metodei și eliminată după ce execuția metodei este finalizată.
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; } } }
Este important aici este de precizat ordinea de executare a aspectelor. Aici, s-a realizat prin atribuirea rolurilor de aspect și ordonarea execuției rolurilor. Nu vrem să fie creat IDbConnection
dacă nu va fi folosit oricum (de exemplu, valorile citite din cache). Este definit de următoarele atribute:
[ProvideAspectRole(StandardRoles.TransactionHandling)] [AspectRoleDependency(AspectDependencyAction.Order, AspectDependencyPosition.After, StandardRoles.Caching)]
PostSharp poate implementa, de asemenea, toate aspectele la nivel de clasă și la nivel de asamblare, deci este important să definiți domeniul de aplicare al atributului:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
Șirul de conexiune este citit din appsettings.json
, dar poate fi suprascris folosind proprietatea statică ConnectionString
.
Fluxul de execuție este următorul:
- Aspect identifică indexul argumentului opțional
IDbConnection
care nu are nicio valoare furnizată. Dacă nu este găsit, omitem. - MySqlConnection este creat pe baza
ConnectionString
furnizată. - Valoarea argumentului
IDbConnection
este setată. - Se numește metoda originală.
Deci, dacă vrem să folosim acest aspect, putem apela doar metoda depozitului fără nicio conexiune furnizată:
await studentRepository.InsertAsync(new Student { Name = "Not Marko Padjen" }, connection: null);
Cache Aspect
Aici dorim să identificăm apeluri unice de metodă și să le memorăm în cache. Apelurile de metodă sunt considerate unice dacă aceeași metodă din aceeași clasă a fost apelată cu aceiași parametri.
În implementarea de mai jos, pe fiecare metodă, se creează cheia de interceptare pentru apel. Acesta este apoi folosit pentru a verifica dacă valoarea returnată există pe serverul cache. Dacă se întâmplă, este returnat fără a apela metoda originală. Dacă nu este, metoda originală este apelată, iar valoarea returnată este salvată pe serverul cache pentru utilizare ulterioară.
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(", "); } } }
Aici respectăm și ordinea aspectelor. Rolul aspectului este Caching
și este definit pentru a merge după TransactionHandling
:

[ProvideAspectRole(StandardRoles.Caching)] [AspectRoleDependency(AspectDependencyAction.Order, AspectDependencyPosition.Before, StandardRoles.TransactionHandling)]
Domeniul de aplicare al atributului este același ca și pentru aspectul DbConnection:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
Expirarea elementelor din cache poate fi setată pentru fiecare metodă prin definirea câmpului public ExpirySeconds
(implicit este de 5 minute), de exemplu:
[Cache(ExpirySeconds = 2 * 60 /* 2 minutes */)] [DbConnection] public Task<IEnumerable<Student>> GetAllAsync(IDbConnection connection = null) { return connection.GetAllAsync<Student>(); }
Fluxul de execuție este următorul:
- Verificați aspectul dacă instanța este
ICacheAware
, care poate oferi un indicator pentru a omite utilizarea cache-ului pe această instanță de obiect. - Aspect generează o cheie pentru apelul de metodă.
- Aspect deschide conexiunea Redis.
- Dacă valoare există cu cheia generată, valoarea este returnată și executarea metodei inițiale este omisă.
- Dacă valoarea nu există, metoda inițială este apelată și valoarea returnată este salvată în cache cu o cheie generată.
Pentru generarea cheii se aplică câteva restricții aici:
-
IDbConnection
ca parametru este întotdeauna ignorat, fiind nul sau nu. Acest lucru se face intenționat pentru a se adapta la utilizarea aspectului anterior. - Valorile speciale ca și valori de șir pot provoca citirea greșită din cache, cum ar fi valorile
<IGNORED>
și<NULL>
. Acest lucru poate fi evitat prin codificarea valorii. - Tipurile de referință nu sunt luate în considerare, doar tipul lor (
.ToString()
este utilizat la evaluarea valorii). În majoritatea cazurilor, acest lucru este în regulă și nu adaugă complexitate suplimentară.
Pentru a utiliza cache-ul în mod corespunzător, ar putea fi necesar să invalidați memoria cache înainte de a expira, cum ar fi la actualizarea entității sau la ștergerea entității.
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; } }
Metoda de ajutor InvalidateCache
acceptă expresia, deci pot fi folosite metacaracterele (similar ca cadrul Moq):
this.InvalidateCache(r => r.GetAsync(student.Id, Any<IDbConnection>()));
Acest aspect este utilizat fără parametri speciali, așa că dezvoltatorii ar trebui să fie conștienți doar de limitările codului.
Punând totul laolaltă
Cel mai bun mod este să îl încercați și să depanați este prin utilizarea testelor de integrare furnizate în proiectul Paden.Aspects.DAL.Tests
.
Următoarea metodă de testare a integrării utilizează servere reale (bază de date relațională și cache). Fațada de conectare este utilizată numai pentru a ține evidența apelurilor de metodă.
[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); }
Baza de date este creată și eliminată automat utilizând fixture de clasă:
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(); } } }
O verificare manuală poate fi efectuată în timpul depanării numai pentru că, după executarea testelor, baza de date este ștearsă și memoria cache este invalidată manual.
De exemplu, în timpul execuției testului Get_Should_Call_Database_If_Entity_Not_Dirty_Otherwise_Read_From_Cache
, putem găsi următoarele valori în baza de date 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\"}"
Testul de integrare GetAllAsync_Should_Not_Call_Database_On_Second_Call
se asigură, de asemenea, că apelurile din cache sunt mai performante decât apelurile originale la sursa de date. De asemenea, produc o urmă care ne spune cât timp a durat pentru a executa fiecare apel:
Database run time (ms): 73 Cache run time (ms): 9
Îmbunătățiri înainte de utilizare în producție
Codul furnizat aici este realizat în scop educațional. Înainte de a-l folosi în sistemul real, pot fi făcute unele îmbunătățiri:
- Aspect DbConnection:
- Pool de conexiuni poate fi implementat dacă este necesar.
- Pot fi implementate mai multe șiruri de conexiune. Utilizarea obișnuită pentru aceasta este clusterul de baze de date relaționale, unde avem tipuri de conexiune numai citire și scriere separată.
- Aspect cache:
- Pool de conexiuni poate fi implementat dacă este necesar.
- Valorile tipului de referință pot fi, de asemenea, considerate ca parte a cheii generate, în funcție de cazul de utilizare. În cele mai multe cazuri, probabil ar oferi doar un dezavantaj de performanță.
Aceste caracteristici nu au fost implementate aici deoarece sunt legate de cerințele specifice ale sistemului în care sunt utilizate și, dacă nu sunt implementate corect, nu ar contribui la performanța sistemului.
Concluzie
Se poate argumenta că „responsabilitatea unică”, „deschis-închis” și „inversarea dependenței” din principiul principiilor SOLID pot fi mai bine implementate cu AOP decât cu OOP. Faptul este că scopul dezvoltatorilor .NET ar trebui să fie o bună organizare a codului, care poate fi atinsă cu multe instrumente, cadre și modele aplicabile unor situații specifice.
Doar pentru a reitera: tot codul din acest articol, inclusiv aspectele și testele de integrare, poate fi găsit în depozitul GitHub notmarkopadjen/dot-net-aspects-postsharp
. Pentru țesutul IL, am folosit PostSharp de pe piața Visual Studio. Codul include un laborator realizat cu docker compose folosind MariaDB 10.4 și Redis 5.0.