Cache e manipulação de conexões no .NET: um tutorial de programação orientada a aspectos

Publicados: 2022-03-11

Ninguém gosta de código clichê. Geralmente, reduzimos usando padrões comuns de programação orientada a objetos, mas geralmente a sobrecarga de código do uso de padrões é quase a mesma — se não maior — do que se tivéssemos usado o código clichê em primeiro lugar. Seria muito bom de alguma forma apenas marcar parte do código que deve implementar determinado comportamento e resolver a implementação em outro lugar.

Por exemplo, se tivermos um StudentRepository , podemos usar o Dapper para obter todos os alunos de um banco de dados relacional:

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

Esta é uma implementação muito simples de um repositório de banco de dados relacional. Se a lista de alunos não muda muito e é chamada com frequência, podemos armazenar em cache esses itens para otimizar o tempo de resposta do nosso sistema. Como geralmente temos muitos repositórios (independentemente de serem relacionais ou não) em nosso código, seria bom deixar essa preocupação transversal de cache de lado e utilizá-la com muita facilidade, como:

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

Um bônus seria não se preocupar com conexões de banco de dados. Deixe essa preocupação transversal de lado também e apenas rotule um método para usar o gerenciador de conexões externas, como:

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

Neste artigo, consideraremos o uso de padrões orientados a aspectos em vez da OOP comumente usada. Embora o AOP já exista há algum tempo, os desenvolvedores geralmente preferem o OOP ao invés do AOP. Embora tudo o que você faz com AOP também possa ser feito com OOP, como programação procedural vs. OOP, o AOP oferece aos desenvolvedores mais opções nos paradigmas que podem usar. O código AOP é organizado de forma diferente, e alguns podem argumentar melhor, em certos aspectos (trocadilhos) do que OOP. No final, a escolha de qual paradigma usar é preferência pessoal.

Como fazemos isso

Em .NET, os padrões AOP podem ser implementados usando entrelaçamento de linguagem intermediária, mais conhecido como entrelaçamento IL . Este é um processo que é iniciado após a compilação do código e altera o código IL produzido por um compilador, para que o código atinja o comportamento esperado. Então, olhando para o exemplo já mencionado, mesmo que não tenhamos escrito código para cache nesta classe, o método que escrevemos será alterado (ou substituído) para chamar o código de cache. A título de ilustração, o resultado final deve ser algo como isto:

 // 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?()); } }

Ferramentas necessárias

Todo o código deste artigo, incluindo aspectos e testes de integração, pode ser encontrado no repositório GitHub notmarkopadjen/dot-net-aspects-postsharp . Para tecelagem IL, usaremos PostSharp do mercado Visual Studio. É uma ferramenta comercial e é necessária uma licença para fins comerciais. Para experimentar, você pode selecionar a licença PostSharp Essentials, que é gratuita.

Se você deseja executar os testes de integração, precisará do servidor MySQL e Redis. No código acima, fiz um laboratório com o Docker Compose usando MariaDB 10.4 e Redis 5.0. Para usá-lo, você precisará instalar o Docker e inicializar a configuração do Compose:

 docker-compose up -d

Você pode, é claro, usar outros servidores e alterar as strings de conexão em appsettings.json .

Codificação Orientada a Aspectos Básicos

Vamos experimentar o padrão de interceptação do AOP. Para fazer isso no PostSharp, precisamos implementar um novo atributo, herdar o atributo MethodInterceptionAspect e substituir os métodos necessários.

 [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); // ... } }

Vemos que temos dois métodos diferentes para chamadas sincronizadas e assíncronas. É importante implementá-los adequadamente para aproveitar ao máximo os recursos assíncronos do .NET. Ao ler do Redis usando a biblioteca StackExchange.Redis , usamos chamadas de método StringGet ou StringGetAsync , dependendo se estamos em ramificação de código sincronizado ou assíncrono.

O fluxo de execução de código é afetado pela invocação de métodos de MethodInterceptionArgs , objeto args e valores de configuração para as propriedades do objeto. Membros mais importantes:

  • Método Proceed ( ProceedAsync ) - Invoca a execução do método original.
  • Propriedade ReturnValue - Contém o valor de retorno da chamada do método. Antes da execução do método original, ele está vazio e depois contém o valor de retorno original. Ele pode ser substituído a qualquer momento.
  • Propriedade do Method - System.Reflection.MethodBase (geralmente System.Reflection.MethodInfo ) contém informações de reflexão do método de destino.
  • Propriedade Instance - Objeto de destino (instância pai do método).
  • Propriedade Arguments - Contém valores de argumento. Ele pode ser substituído a qualquer momento.

O aspecto DbConnection

Queremos poder chamar métodos de repositório sem uma instância de IDbConnection e deixar o aspecto criar essas conexões e fornecê-las à chamada do método. Às vezes, você pode querer fornecer a conexão de qualquer maneira (por exemplo, por causa de transações) e, nessas ocasiões, o aspecto não deve fazer nada.

Na implementação abaixo, teremos código apenas para gerenciamento de conexão de banco de dados, como teríamos em qualquer repositório de entidade de banco de dados. Nesse caso específico, uma instância de MySqlConnection é analisada para a execução do método e descartada após a conclusão da execução do método.

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

O importante aqui é especificar a ordem de execução dos aspectos. Aqui, isso foi feito atribuindo funções de aspecto e ordenando a execução de funções. Não queremos que IDbConnection seja criado se não for usado de qualquer maneira (por exemplo, valores lidos do cache). Ele é definido pelos seguintes atributos:

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

PostSharp também pode implementar todos os aspectos em nível de classe e nível de montagem, por isso é importante definir o escopo do atributo:

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

A cadeia de conexão está sendo lida em appsettings.json , mas pode ser substituída usando a propriedade estática ConnectionString .

O fluxo de execução é o seguinte:

  1. Aspect identifica o índice de argumento opcional IDbConnection que não tem valor fornecido. Se não for encontrado, pulamos.
  2. MySqlConnection é criado com base no ConnectionString fornecido.
  3. O valor do argumento IDbConnection está definido.
  4. O método original é chamado.

Então, se quisermos usar esse aspecto, podemos apenas chamar o método do repositório sem conexão fornecida:

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

Aspecto do Cache

Aqui queremos identificar chamadas de método exclusivas e armazená-las em cache. As chamadas de método são consideradas exclusivas se o mesmo método da mesma classe tiver sido chamado com os mesmos parâmetros.

Na implementação abaixo, em cada método, a chave de interceptação é criada para a chamada. Isso é usado para verificar se o valor de retorno existe no servidor de cache. Se isso acontecer, ele será retornado sem chamar o método original. Se não for, o método original é chamado e o valor retornado é salvo no servidor de cache para uso posterior.

 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(", "); } } }

Aqui, também respeitamos a ordem dos aspectos. A função de aspecto é Caching , e é definida para ir depois de TransactionHandling :

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

O escopo do atributo é o mesmo do aspecto DbConnection:

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

A expiração de itens em cache pode ser definida em cada método definindo o campo público ExpirySeconds (o padrão é 5 minutos), por exemplo:

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

O fluxo de execução é o seguinte:

  1. Verificação de aspecto se a instância é ICacheAware que pode fornecer um sinalizador para pular o uso do cache nesta determinada instância de objeto.
  2. Aspect gera uma chave para a chamada do método.
  3. Aspect abre a conexão Redis.
  4. Se o valor existir com a chave gerada, o valor será retornado e a execução do método original será ignorada.
  5. Se value não existir, o método original é chamado e o valor de retorno é salvo no cache com uma chave gerada.

Para geração de chave, algumas restrições se aplicam aqui:

  1. IDbConnection como parâmetro é sempre ignorado, sendo nulo ou não. Isso é feito de propósito para acomodar o uso do aspecto anterior.
  2. Valores especiais como valores de string podem causar leitura incorreta do cache, como valores <IGNORED> e <NULL> . Isso pode ser evitado com codificação de valor.
  3. Os tipos de referência não são considerados, apenas seu tipo ( .ToString() é usado na avaliação de valor). Para a maioria dos casos, isso é bom e não adiciona complexidade adicional.

Para usar o cache corretamente, pode ser necessário invalidar o cache antes que ele expire, como na atualização da entidade ou na exclusão da entidade.

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

O método auxiliar InvalidateCache aceita expressão, portanto, curingas podem ser usados ​​(semelhante ao framework Moq):

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

Este aspecto está sendo usado sem parâmetros especiais, portanto, os desenvolvedores devem estar cientes apenas das limitações do código.

Juntando tudo

A melhor maneira é experimentar e depurar usando os testes de integração fornecidos no projeto Paden.Aspects.DAL.Tests .

O método de teste de integração a seguir usa servidores reais (banco de dados relacional e cache). A fachada de conexão é utilizada apenas para acompanhar as chamadas de método.

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

O banco de dados é criado e descartado automaticamente usando o acessório de classe:

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

Uma verificação manual pode ser realizada durante a depuração apenas porque, após a execução dos testes, o banco de dados é excluído e o cache é invalidado manualmente.

Por exemplo, durante a execução do teste Get_Should_Call_Database_If_Entity_Not_Dirty_Otherwise_Read_From_Cache , podemos encontrar os seguintes valores no banco de dados 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\"}"

O teste de integração GetAllAsync_Should_Not_Call_Database_On_Second_Call também está certificando-se de que as chamadas em cache tenham melhor desempenho do que as chamadas de fonte de dados originais. Eles também produzem rastreamento que nos diz quanto tempo levou para executar cada chamada:

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

Melhorias antes de usar na produção

O código fornecido aqui é feito para fins educacionais. Antes de usá-lo no sistema do mundo real, algumas melhorias podem ser feitas:

  • Aspecto DbConnection:
    • O pool de conexões pode ser implementado, se necessário.
    • Várias cadeias de conexão podem ser implementadas. O uso comum para isso é o cluster de banco de dados relacional, onde distinguimos os tipos de conexão somente leitura e leitura-gravação.
  • Aspecto de cache:
    • O pool de conexões pode ser implementado, se necessário.
    • Os valores do tipo de referência também podem ser considerados como parte da chave gerada, dependendo do caso de uso. Na maioria dos casos, eles provavelmente forneceriam apenas uma desvantagem de desempenho.

Esses recursos não foram implementados aqui porque estão relacionados aos requisitos específicos do sistema em que estão sendo usados ​​e, se não implementados adequadamente, não contribuiriam para o desempenho do sistema.

Conclusão

Pode-se argumentar que “responsabilidade única”, “aberto-fechado” e “inversão de dependência” do princípio dos princípios SOLID podem ser melhor implementados com AOP do que com OOP. O fato é que o objetivo dos desenvolvedores .NET deve ser uma boa organização do código, que pode ser alcançada com muitas ferramentas, frameworks e padrões aplicáveis ​​a situações específicas.

Apenas para reiterar: Todo o código deste artigo, incluindo aspectos e testes de integração, pode ser encontrado no repositório GitHub notmarkopadjen/dot-net-aspects-postsharp . Para a tecelagem IL, usamos PostSharp do mercado Visual Studio. O código inclui um laboratório feito com docker compose usando MariaDB 10.4 e Redis 5.0.