个性化阅读
专注于IT技术分析

.NET中的缓存和连接处理:面向方面的编程教程

本文概述

没有人喜欢样板代码。我们通常通过使用通用的面向对象的编程模式来减少这种情况, 但是使用模式的代码开销通常比起初使用样板代码几乎相同(如果不是更大的话)。仅以某种方式标记应实现某些行为的代码部分, 并在其他地方解决实现问题, 这将是非常好的。

例如, 如果我们有一个StudentRepository, 则可以使用Dapper从关系数据库中获取所有学生:

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

这是关系数据库存储库的非常简单的实现。如果学生名单变化不大且经常被调用, 我们可以缓存这些项目以优化系统的响应时间。由于我们的代码中通常会有很多存储库(无论它们是否是关系型的), 因此最好将缓存这一跨领域的问题放在一边, 并非常容易地利用它, 例如:

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

一个好处就是不用担心数据库连接。也要把这个跨领域的问题放在一旁, 只是标记一个使用外部连接管理器的方法, 例如:

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

在本文中, 我们将考虑面向方面的模式用法, 而不是常用的OOP。尽管AOP已经存在了一段时间, 但是与AOP相比, 开发人员通常更喜欢OOP。尽管使用AOP进行的所有操作也都可以使用OOP完成, 例如过程编程与OOP, 但AOP为开发人员提供了更多可用的范例选择。与OOP相比, AOP代码在某些方面(双关语意)的组织方式不同, 并且有些人可能会争论得更好。最后, 使用哪种范式的选择是个人喜好。

我们如何做

在.NET中, 可以使用中间语言编织(通常称为IL编织)来实现AOP模式。这是一个在代码编译后启动的过程, 它会更改由编译器生成的IL代码, 以使代码达到预期的行为。因此, 看看已经提到的示例, 即使我们没有在此类中编写用于缓存的代码, 也将更改(或替换)我们编写的方法以调用缓存代码。为了说明起见, 最终结果应如下所示:

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

所需工具

可以从notmarkopadjen / dot-net-aspects-postsharp GitHub存储库中找到本文中的所有代码, 包括方面和集成测试。对于IL编织, 我们将使用Visual Studio市场中的PostSharp。它是一种商业工具, 出于商业目的需要许可证。为了进行试验, 你可以选择免费的PostSharp Essentials许可证。

如果要运行集成测试, 则需要MySQL和Redis服务器。在上面的代码中, 我使用MariaDB 10.4和Redis 5.0在Docker Compose上进行了实验。为了使用它, 你将需要安装Docker并启动Compose配置:

docker-compose up -d

当然, 你可以使用其他服务器并在appsettings.json中更改连接字符串。

面向方面的基本编码

让我们尝试一下AOP的拦截模式。为此, 我们需要在PostSharp中实现一个新属性, 继承MethodInterceptionAspect属性并覆盖所需的方法。

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

我们看到我们有两种不同的同步和异步调用方法。重要的是要正确实现这些功能, 以充分利用.NET异步功能。使用StackExchange.Redis库从Redis读取数据时, 我们使用StringGet或StringGetAsync方法调用, 具体取决于我们处于同步还是异步代码分支。

调用MethodInterceptionArgs, args对象的方法以及将值设置为对象属性会影响代码执行流程。最重要的成员:

  • 继续(ProceedAsync)方法-调用原始方法执行。
  • ReturnValue属性-包含方法调用的返回值。在原始方法执行之前, 它为空, 在它包含原始返回值之后。可以随时更换。
  • 方法属性-System.Reflection.MethodBase(通常为System.Reflection.MethodInfo)包含目标方法的反射信息。
  • 实例属性-目标对象(方法父实例)。
  • Arguments属性-包含参数值。可以随时更换。

DbConnection方面

我们希望能够在没有IDbConnection实例的情况下调用存储库方法, 并让方面创建这些连接并将其提供给方法调用。有时, 你可能仍想提供连接(例如, 由于交易), 在这种情况下, 方面应该什么也不做。

在下面的实现中, 我们将仅具有数据库连接管理的代码, 就像在任何数据库实体存储库中一样。在这种特定情况下, 将MySqlConnection的实例解析为方法执行, 并在方法执行完成后将其丢弃。

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

这里重要的是指定各方面的执行顺序。在这里, 它是通过分配方面角色并排序角色执行来完成的。如果我们还是不想使用IDbConnection(例如, 从缓存中读取的值), 则我们不希望创建它。它由以下属性定义:

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

PostSharp还可以在类级别和程序集级别实现所有方面, 因此定义属性范围很重要:

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

正在从appsettings.json中读取连接字符串, 但可以使用静态属性ConnectionString覆盖该连接字符串。

执行流程如下:

  1. Aspect标识没有提供值的IDbConnection可选参数索引。如果找不到, 我们将跳过。
  2. MySqlConnection是基于提供的ConnectionString创建的。
  3. IDbConnection参数值已设置。
  4. 原始方法被调用。

因此, 如果要使用此方面, 我们可以在不提供连接的情况下调用存储库方法:

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

缓存方面

在这里, 我们要确定唯一的方法调用并对其进行缓存。如果使用相同的参数调用了同一类中的同一方法, 则该方法调用被视为唯一。

在下面的实现中, 在每种方法上, 都会为该调用创建拦截密钥。然后用于检查缓存服务器上是否存在返回值。如果是这样, 则不调用原始方法就将其返回。如果不是, 则调用原始方法, 并将返回的值保存到缓存服务器以供进一步使用。

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

在这里, 我们还尊重各个方面的顺序。方面角色是”缓存”, 它定义为在TransactionHandling之后执行:

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

该属性的作用域与DbConnection方面的作用域相同:

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

可以通过定义公共字段ExpirySeconds(默认值为5分钟)在每种方法上设置缓存项的到期时间, 例如:

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

执行流程如下:

  1. 方面检查实例是否为ICacheAware, 它可以提供一个标志以跳过对此特定对象实例使用缓存的操作。
  2. Aspect会为方法调用生成一个密钥。
  3. Aspect将打开Redis连接。
  4. 如果值与生成的键一起存在, 则返回值, 并跳过原始方法的执行。
  5. 如果value不存在, 则调用原始方法, 并使用生成的键将返回值保存在缓存中。

对于密钥生成, 此处有一些限制:

  1. IDbConnection作为参数始终被忽略, 是否为null。这样做是为了适应先前方面的使用。
  2. 特殊值(例如字符串值)可能导致从缓存中读取错误, 例如<IGNORED>和<NULL>值。使用值编码可以避免这种情况。
  3. 不考虑引用类型, 仅考虑它们的类型(在值评估中使用.ToString())。在大多数情况下, 这很好, 并且不会增加其他复杂性。

为了正确使用缓存, 可能需要在缓存过期之前使缓存无效, 例如实体更新或实体删除。

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 helper方法接受表达式, 因此可以使用通配符(类似于Moq框架):

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

使用此方面时没有特殊参数, 因此开发人员应仅了解代码限制。

放在一起

最好的方法是通过使用Paden.Aspects.DAL.Tests项目中提供的集成测试来进行尝试和调试。

以下集成测试方法使用真实的服务器(关系数据库和缓存)。连接外观仅用于跟踪方法调用。

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

该数据库是使用类fixture自动创建和处理的:

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

只能在调试期间执行手动检查, 这是因为在执行测试之后, 将删除数据库并手动使缓存无效。

例如, 在执行Get_Should_Call_Database_If_Entity_Not_Dirty_Otherwise_Read_From_Cache测试期间, 我们可以在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\"}"

集成测试GetAllAsync_Should_Not_Call_Database_On_Second_Call还可以确保缓存的调用比原始数据源调用的性能更好。它们还会产生跟踪, 告诉我们执行每个调用花费了多少时间:

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

生产前的改进

此处提供的代码用于教育目的。在实际系统中使用它之前, 可能需要做一些改进:

  • DbConnection方面:
    • 如果需要, 可以实现连接池。
    • 可以实现多个连接字符串。关系数据库群集的常见用法是我们区分只读和读写连接类型。
  • 缓存方面:
    • 如果需要, 可以实现连接池。
    • 根据使用情况, 引用类型值也可以视为生成的键的一部分。在大多数情况下, 它们可能只会带来性能缺陷。

这些功能未在此处实现, 因为它们与所使用的系统的特定要求有关, 如果未正确实现, 则不会对系统的性能有所帮助。

总结

有人可能会说, 使用AOP而不是使用OOP可以更好地实现SOLID原则的”单一责任”, “开放-封闭”和”依赖倒置”。事实是, .NET开发人员的目标应该是良好的代码组织, 这可以通过许多适用于特定情况的工具, 框架和模式来实现。

重申一下:本文的所有代码, 包括方面和集成测试, 都可以在notmarkopadjen / dot-net-aspects-postsharp GitHub存储库中找到。对于IL编织, 我们使用了Visual Studio市场中的PostSharp。该代码包括一个使用MariaDB 10.4和Redis 5.0使用docker compose进行的实验。

赞(0)
未经允许不得转载:srcmini » .NET中的缓存和连接处理:面向方面的编程教程

评论 抢沙发

评论前必须登录!