作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Marko Pađen的头像

Marko Pađen

Marko在包括架构在内的大型软件方面拥有超过十年的经验, design, coding, testing, documentation, and release.

Expertise

Previously At

IGT
Share

没有人喜欢样板代码. 我们通常通过使用常见的面向对象编程模式来减少它, 但是,通常使用模式的代码开销与我们最初使用样板代码时几乎相同,如果不是更大的话. 以某种方式标记应该实现某些行为的部分代码将是非常好的, and resolve implementation somewhere else.

例如,如果我们有 StudentRepository, we can use Dapper to get all the students from a relational database:

公共类StudentRepository
{
    public Task> GetAllAsync(IDbConnection connection)
    {
        return connection.GetAllAsync();
    }
}

这是关系数据库存储库的一个非常简单的实现. 如果学生名单变化不大并且经常被调用, 我们可以缓存这些项来优化系统的响应时间. 因为我们的代码中通常有很多存储库(不管它们是否关系), 把缓存的横切问题放在一边,并很容易地利用它,这是很好的, like:

公共类StudentRepository
{
    [Cache]
    public Task> GetAllAsync(IDbConnection connection)
    {
        return connection.GetAllAsync();
    }
}

A bonus would be not to worry about database connections. Have this cross-cutting concern aside as well, and just label a method to use external connection manager, like:

公共类StudentRepository
{
    [Cache]
    [DbConnection]
    public Task> GetAllAsync(IDbConnection = null
    {
        return connection.GetAllAsync();
    }
}

在本文中,我们将考虑使用面向方面的模式,而不是常用的OOP. 尽管AOP已经存在了一段时间,但开发人员通常更喜欢OOP而不是AOP. 虽然你用AOP做的所有事情都可以用OOP来做,比如过程编程和面向对象编程. 面向对象、AOP为开发人员提供了更多可以使用的范例选择. AOP代码的组织方式不同, 有些人可能会辩称得更好, on certain aspects (pun intended) than OOP. 最后,选择使用哪种范式是个人偏好.

How We Do It

In .. NET中,AOP模式可以使用中间语言编织实现,更广为人知的是 IL weaving. 这是一个在代码编译后启动的过程, and it changes the IL code produced by a compiler, to make the code achieve expected behavior. So, looking at the example already mentioned, 尽管我们没有在这门课中编写缓存代码, 为了调用缓存代码,我们编写的方法将被更改(或替换). 为了便于说明,最终结果应该是这样的:

//由PostSharp编织

公共类StudentRepository
{
    [DebuggerTargetMethod (100663306)]
    [DebuggerBindingMethod (100663329)]
    [DebuggerBindingMethod (100663335)]
    public async Task> GetAllAsync(
      IDbConnection = null
    {
      AsyncMethodInterceptionArgsImpl> 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) interceptionArgsImpl.TypedReturnValue;
    }

    [DebuggerSourceMethod (100663300)]
    private Task> z__OriginalMethod(
      [可选]IDbConnection连接
    {
      return (Task>) SqlMapperExtensions.GetAllAsync(connection, (IDbTransaction) null, new int?());
    }
}

Tools Required

本文中的所有代码,包括方面和集成测试,都可以在 notmarkopadjen/dot-net-aspects-postsharp GitHub repository. 对于IL编织,我们将使用 PostSharp 从Visual Studio市场. 这是一个商业工具,商业用途需要获得许可. 为了进行实验,您可以选择PostSharp Essentials许可证,它是免费的.

如果你想运行集成测试,你将需要MySQL和Redis服务器. 在上面的代码中,我使用MariaDB 10做了一个Docker Compose实验.4 and Redis 5.0. In order to use it, you will need to install Docker 组合配置:

docker-compose up -d

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

基本面向方面的编码

Let’s try out AOP’s interception pattern. 要在PostSharp中做到这一点,我们需要实现一个新属性,继承 MethodInterceptionAspect attribute and override required methods.

[PSerializable]
[AttributeUsage AttributeTargets.类| AttributeTargets.Method)]
public class CacheAttribute : MethodInterceptionAspect
{
    // ...

    OnInvoke(MethodInterceptionArgs)
    {
        // ...
var redisValue = db.StringGet(key);
        // ...
    }
    
    OnInvokeAsync(MethodInterceptionArgs)
    {
        // ...
var redisValue =等待db.StringGetAsync(key);
        // ...
    }
}

我们看到,我们有两种不同的同步和异步调用方法. 重要的是要正确地实施这些措施,以充分利用它们 .NET async features. 从Redis读取时使用 StackExchange.Redis library, we use StringGet or StringGetAsync 方法调用,这取决于我们是在同步还是异步代码分支中.

Code execution flow is affected by invoking methods of MethodInterceptionArgs, args 对象,并为该对象的属性设置值. 最重要的成员:

  • Proceed (ProceedAsync) method - Invokes original method execution.
  • ReturnValue property - Contains the return value of the method call. 在原始方法执行之前,它是空的,在它包含原始返回值之后. 可随时更换.
  • Method property - System.Reflection.MethodBase (usually System.Reflection.MethodInfo) contains target method reflection information.
  • Instance property - Target object (method parent instance).
  • Arguments 属性-包含参数值. 可随时更换.

DbConnection方面

我们希望能够在没有实例的情况下调用存储库方法 IDbConnection,让方面创建这些连接,并将其提供给方法调用. 有时,您可能希望无论如何都提供连接(例如.g.(因为事务),在这种情况下,方面不应该做任何事情.

在下面的实现中, we will have code only for database connection management, as we would have in any database entity repository. In this particular case, an instance of 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.秩序,AspectDependencyPosition.After, StandardRoles.Caching)]
    [AttributeUsage AttributeTargets.类| AttributeTargets.Method)]
    DbConnectionAttribute: MethodInterceptionAspect
    {
        const string DefaultConnectionStringName = "DefaultConnection";

        static Lazy config;

        连接字符串;
        公共静态字符串ConnectionString
        {
            返回连接字符串 ?? config.Value.GetConnectionString(DefaultConnectionStringName); }
            set { connectionString = value; }
        }

        静态DbConnectionAttribute ()
        {
            config = new Lazy(() => new ConfigurationBuilder().AddJsonFile(“appsettings.json", false, false).Build());
        }

        OnInvoke(MethodInterceptionArgs)
        {
            var i = GetArgumentIndex(args);
            if (!i.HasValue)
            {
                args.Proceed();
                return;
            }

            使用(IDbConnection db = new MySqlConnection(ConnectionString))
            {
                args.Arguments.SetArgument(i.Value, db);
                args.Proceed();
            }
        }

        OnInvokeAsync(MethodInterceptionArgs)
        {
            var i = GetArgumentIndex(args);
            if (!i.HasValue)
            {
                await args.ProceedAsync();
                return;
            }

            使用(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参数=参数[i];
                if (parameter.ParameterType == typeof(IDbConnection)
                    && parameter.IsOptional
                    && args.Arguments[i] == null)
                {
                    return i;
                }
            }
            return null;
        }
    }
}

在这里,重要的是指定方面的执行顺序. 在这里,它是通过分配方面角色和排序角色执行来完成的. We don’t want IDbConnection to be created if it’s not going to be used anyway (e.g.(从缓存中读取的值). It is defined by the following attributes:

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

PostSharp can also implement all aspects on class level, and assembly level, so it’s important to define attribute scope:

[AttributeUsage AttributeTargets.类| AttributeTargets.Method)]

The connection string is being read from appsettings.json, but can be overridden using static property ConnectionString.

执行流程如下:

  1. Aspect identifies IDbConnection optional argument index which has no value provided. 如果没有找到,我们跳过.
  2. MySqlConnection is created based on provided ConnectionString.
  3. IDbConnection argument value is set.
  4. 调用原始方法.

So, 如果我们想用这个方面, 我们可以在不提供连接的情况下调用repository方法:

等待studentRepository.InsertAsync(新学生
{
    名字=“不是马尔科·帕德扬”
}, connection: null);

Cache Aspect

这里我们想要识别唯一的方法调用并缓存它们. 如果使用相同的参数调用同一类的相同方法,则认为方法调用是唯一的.

在下面的实现中,在每个方法上,都为调用创建了拦截键. 然后用它来检查返回值是否存在于缓存服务器上. 如果是,则返回它而不调用原始方法. If it’s not, 调用原始方法, 返回的值保存到缓存服务器以供进一步使用.

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.秩序,AspectDependencyPosition.之前,StandardRoles.TransactionHandling)]
    [AttributeUsage AttributeTargets.类| AttributeTargets.Method)]
    public class CacheAttribute : MethodInterceptionAspect
    {
        const int DefaultExpirySeconds = 5 * 60;

        static Lazy redisServer;

        public int ExpirySeconds = DefaultExpirySeconds;
        private TimeSpan? Expiry => ExpirySeconds == -1 ? (TimeSpan?)null : TimeSpan.FromSeconds (ExpirySeconds);

        静态CacheAttribute ()
        {
            redisServer = new Lazy(() => new ConfigurationBuilder().AddJsonFile(“appsettings.json", false, false).Build()(“复述:服务器”));
        }        

        OnInvoke(MethodInterceptionArgs)
        {
            if (args.实例为ICacheAware cacheAware && !cacheAware.CacheEnabled)
            {
                args.Proceed();
                return;
            }

            var key = GetKey(args.方法作为MethodInfo,参数.Arguments);

            using (var connection = ConnectionMultiplexer.Connect(redisServer.Value))
            {
                var db = connection.GetDatabase();
                var redisValue = db.StringGet(key);

                if (redisValue.IsNullOrEmpty)
                {
                    args.Proceed();
                    db.StringSet(关键,JsonConvert.SerializeObject(args.ReturnValue),到期);
                }
                else
                {
                    args.ReturnValue = JsonConvert.DeserializeObject (redisValue.ToString(), (args.Method as MethodInfo).ReturnType);
                }
            }
        }

        OnInvokeAsync(MethodInterceptionArgs)
        {
            if (args.实例为ICacheAware cacheAware && !cacheAware.CacheEnabled)
            {
                await args.ProceedAsync();
                return;
            }

            var key = GetKey(args.方法作为MethodInfo,参数.Arguments);

            using (var connection = ConnectionMultiplexer.Connect(redisServer.Value))
            {
                var db = connection.GetDatabase();
                var redisValue =等待db.StringGetAsync(key);

                if (redisValue.IsNullOrEmpty)
                {
                    await args.ProceedAsync();
                    db.StringSet(关键,JsonConvert.SerializeObject(args.ReturnValue),到期);
                }
                else
                {
                    args.ReturnValue = JsonConvert.DeserializeObject (redisValue.ToString(), (args.Method as MethodInfo).ReturnType.GenericTypeArguments [0]);
                }
            }
        }

        private string GetKey(MethodInfo method, IList values)
        {
            Var参数=方法.GetParameters();
            var keyBuilder = GetKeyBuilder(method);
            keyBuilder.Append("(");
            Foreach(参数中的var参数)
            {
                AppendParameterValue(keyBuilder, parameter, values.Position]);
            }
            if (parameters.Any())
            {
                keyBuilder.Remove(keyBuilder.Length - 2, 2);
            }
            keyBuilder.Append(")");

            return keyBuilder.ToString();
        }

        public static void InvalidateCache(Expression> expression)
        {
            var methodCallExpression =表达式.主体作为methodcalexpression;
            var keyBuilder = GetKeyBuilder(methodCallExpression.Method);
            var参数= methodcalexpression.Method.GetParameters();

            var anyMethod = typeof(CacheExtensions).GetMethod (nameof (CacheExtensions.Any));

            keyBuilder.Append("(");
            for (int i = 0; i < parameters.Length; i++)
            {
                Var参数=参数[i];
                var参数= methodcalexpression.Arguments[i];

                object value = null;

                if (argument is ConstantExpression constantArgument)
                {
                    value =常量参数.Value;
                }
               else if (argument is MemberExpression memberArgument)
                {
                    value = Expression.λ(memberArgument).Compile().DynamicInvoke();
                }
                如果(参数是MethodCallExpression)
                {
                    如果(methodCallArgument.Method == anyMethod.MakeGenericMethod (methodCallArgument.Method.GetGenericArguments ()))
                    {
                        value = "*";
                    }
                }

                AppendParameterValue(keyBuilder, parameter, value);
            }
            如果(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]) 
                对于i=1, #keys, 5000 
                redis.调用('del', unpack(keys, i, math).Min (i + 4999, #keys))
                end", values: new RedisValue[] { CacheExtensions.EscapeRedisString (keyBuilder.ToString()) });
            }
        }

        GetKeyBuilder(MethodInfo方法)
        {
            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;
        }

        AppendParameterValue(StringBuilder, ParameterInfo参数, object value)
        {
            keyBuilder.Append(parameter.ParameterType.FullName);
            keyBuilder.Append(" ");
            if (parameter.ParameterType == typeof(IDbConnection))
            {
                keyBuilder.Append("");
            }
            else
            {
                keyBuilder.Append(value == null ? "" : value.ToString());
            }
            keyBuilder.Append(", ");
        }
    }
}


Here, we also respect order of the aspects. The aspect role is Caching,它被定义为追求 TransactionHandling:

[ProvideAspectRole StandardRoles.Caching)]
[AspectRoleDependency(AspectDependencyAction.秩序,AspectDependencyPosition.之前,StandardRoles.TransactionHandling)]

属性范围与DbConnection方面相同:

[AttributeUsage AttributeTargets.类| AttributeTargets.Method)]

可以通过定义公共字段在每个方法上设置缓存项的过期时间 ExpirySeconds (默认为5分钟.g.:

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

执行流程如下:

  1. 方面检查实例是否为 ICacheAware 哪一个可以提供一个标志,以跳过在此特定对象实例上使用缓存.
  2. Aspect generates a key for the method call.
  3. Aspect打开Redis连接.
  4. 如果value与生成的键一起存在,则返回value并跳过原始方法的执行.
  5. 如果价值不存在, 调用原始方法,并将返回值与生成的键一起保存在缓存中.

For key generation some restrictions here apply:

  1. IDbConnection as a parameter is always ignored, being null or not. 这样做的目的是为了适应前一个方面的使用.
  2. 作为字符串值的特殊值可能导致从缓存中读取错误,例如 and values. This can be avoided with value encoding.
  3. Reference types are not considered, only their type (.ToString() 用于价值评估). 对于大多数情况,这是可以的,并且不会增加额外的复杂性.

以便正确使用缓存, 可能需要在缓存过期之前使其失效, 比如实体更新, or entity deletion.

公共类StudentRepository : ICacheAware
{
    // ...
    
    [Cache]
    [DbConnection]
    public Task> GetAllAsync(IDbConnection = null
    {
        return connection.GetAllAsync();
    }

    [Cache]
    [DbConnection]
    public Task GetAsync(int id, IDbConnection = null
    {
        return connection.GetAsync(id);
    }

    [DbConnection]
    public async Task InsertAsync(Student student, IDbConnection = null
    {
        Var result =等待连接.InsertAsync(学生);
        this.InvalidateCache(r => r.GetAllAsync(Any()));
        return result;
    }

    [DbConnection]
    public async Task UpdateAsync(Student student, IDbConnection = null
    {
        Var result =等待连接.UpdateAsync(student);
        this.InvalidateCache(r => r.GetAllAsync(Any()));
        this.InvalidateCache(r => r.GetAsync(student.Id, Any()));
        return result;
    }

    [DbConnection]
    public async Task DeleteAsync(Student student, IDbConnection = null
    {
        Var result =等待连接.DeleteAsync(student);
        this.InvalidateCache(r => r.GetAllAsync(Any()));
        this.InvalidateCache(r => r.GetAsync(student.Id, Any()));
        return result;
    }
}

InvalidateCache Helper方法接受表达式,因此可以使用通配符(类似于 Moq framework):

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

This aspect is being used with no special parameters, so developers should only be aware of code limitations.

把它们放在一起

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

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

[Fact]
Get_Should_Call_Database_If_Entity_Not_Dirty_Otherwise_Read_From_Cache()
{
    var student =新学生
    {
        Id = studentId,
        名字=“不是马尔科·帕德扬”
    };
    var studentUpdated =新学生
    {
        Id = studentId,
        名称=“未更新马尔科·帕德延”
    };
    等待systemUnderTest.InsertAsync(学生);

    // Gets entity by id, should save in cache
    Assert.Equal(student.名称,(等待systemUnderTest).GetAsync(studentId)).Name);

    // Updates entity by id, should invalidate cache
    等待systemUnderTest.UpdateAsync (studentUpdated);

    var connectionMock = fixture.GetConnectionFacade ();

    // Gets entity by id, ensures that it is the expected one
    Assert.Equal(studentUpdated.名称,(等待systemUnderTest).GetAsync (studentId connectionMock)).Name);

    // Ensures that database was used for the call
    Mock.Get(connectionMock).Verify(m => m.CreateCommand()次.Once);

    var connectionMockUnused = fixture.GetConnectionFacade ();

    // Calls again, should read from cache
    Assert.Equal(studentUpdated.名称,(等待systemUnderTest).GetAsync(studentId, connectionMockUnused)).Name);

    //确保数据库没有被使用
    Mock.Get (connectionMockUnused).Verify(m => m.CreateCommand()次.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}";

        公共DatabaseFixture ()
        {
            var config = new ConfigurationBuilder().AddJsonFile(“appsettings.json", false, false).Build();
            var connectionString =配置.GetConnectionString("DefaultConnection");
            Connection = new MySqlConnection(connectionString);

            Connection.Open();
            新建MySqlCommand($"CREATE DATABASE ' {DatabaseName} ';",连接).ExecuteNonQuery();
            Connection.ChangeDatabase(数据库名);

            DbConnectionAttribute.$"{ConnectionString};数据库={DatabaseName}";
        }

        createtables ()
        {
            新MySqlCommand(学生.ReCreateStatement、连接).ExecuteNonQuery();
        }

        public IDbConnection GetConnectionFacade()
        {
            var connectionMock = Mock.Of();
            Mock.Get(connectionMock).Setup(m => m.CreateCommand()).Returns(Connection.CreateCommand()).Verifiable();
            Mock.Get(connectionMock).SetupGet(m => m.State).返回(ConnectionState.Open).Verifiable();
            返回connectionMock;
        }

        public void Dispose()
        {
            try
            {
                (1) DROP DATABASE IF EXISTS ' {DatabaseName} ';.ExecuteNonQuery();
            }
            catch (Exception)
            {
                // ignored
            }
            Connection.Close();
        }
    }
}

手动检查只能在调试期间执行,因为在执行测试之后, 数据库被删除,缓存被手动失效.

的执行过程中 Get_Should_Call_Database_If_Entity_Not_Dirty_Otherwise_Read_From_Cache test, we can find following values in the Redis database:

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,派登.Aspects.DAL, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null}.Paden.Aspects.DAL.StudentRepository.GetAsync(System.Int32 1, System.Data.IDbConnection )"

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,派登.Aspects.DAL, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null}.Paden.Aspects.DAL.StudentRepository.GetAsync(System.Int32 1, System.Data.IDbConnection )"
"{\"Id\":1,\"Name\":\"Not Marko Padjen\"}"

Integration test GetAllAsync_Should_Not_Call_Database_On_Second_Call 还要确保缓存的调用比原始数据源调用性能更好. 它们还会产生trace,告诉我们执行每个调用所花费的时间:

数据库运行时间(ms): 73
缓存运行时间(ms): 9

Improvements Before Using in Production

The code provided here is made for educational purposes. 在将其应用于实际系统之前,可能需要做一些改进:

  • DbConnection aspect:
    • Connection pool can be implemented if required.
    • Multiple connection strings can be implemented. 它的常见用法是关系数据库集群,其中我们区分只读和读写连接类型.
  • Cache aspect:
    • Connection pool can be implemented if required.
    • 根据用例,引用类型值也可以被视为生成键的一部分. 在大多数情况下,它们可能只会带来性能缺陷.

这里没有实现这些特性,因为它们与使用它们的系统的特定需求相关, 如果没有正确实现,将不会对系统的性能做出贡献.

Conclusion

有人可能会认为,“单一职责”、“开闭”和“依赖倒置”来自于 SOLID principles 用AOP比用OOP可能更好地实现这个原则. 事实是,目标为 .NET developers 应该是良好的代码组织, 哪些可以通过多种工具实现, frameworks, and patterns applicable to specific situations.

Just to reiterate: 本文中的所有代码,包括方面和集成测试,都可以在 notmarkopadjen/dot-net-aspects-postsharp GitHub repository. 对于IL编织,我们使用 PostSharp 从Visual Studio市场. 该代码包括一个使用MariaDB 10的docker编写的实验.4 and Redis 5.0.

了解基本知识

  • What is AOP good for?

    面向方面的编程(AOP)非常擅长允许横切关注点的分离.

  • What is IL weaving?

    IL编织是在源代码编译后更改IL代码的过程.

  • Should I use AOP?

    如果您喜欢这种编码风格、最终结果和您获得的好处——是的.

  • AOP比OOP好吗?

    这个问题的答案与项目密切相关, 代码可重用性需求, 和个人团队成员的偏好一样,有些人说函数式编程可能比面向对象编程更好.

Consult the author or an expert on this topic.
Schedule a call
Marko Pađen的头像
Marko Pađen

Located in Berlin, Germany

Member since August 13, 2019

About the author

Marko在包括架构在内的大型软件方面拥有超过十年的经验, design, coding, testing, documentation, and release.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

Expertise

Previously At

IGT

World-class articles, delivered weekly.

订阅意味着同意我们的 privacy policy

World-class articles, delivered weekly.

订阅意味着同意我们的 privacy policy

Join the Toptal® community.