如何使用 AOP 改进 .NET 应用程序






4.93/5 (17投票s)
了解如何配置依赖注入以按设计实现目标。
其理念是 AOP(面向切面编程)。这种技术广泛应用于 Java,并且能以很低的代价保持高标准的质量。今天,我们将学习如何在 .NET Core 项目中轻松使用它。在简要的理论解释之后,您将看到两个示例(所有代码都在我的 GitHub 个人资料中)。
什么是 AOP
让我们从维基百科的定义开始
引用在计算领域,面向切面编程(AOP)是一种编程范例,旨在通过允许分离横切关注点来提高模块化。它通过在不修改现有代码本身的情况下为现有代码添加附加行为(称为“通知”)来实现……这使得非业务核心逻辑(如日志记录)的行为能够被添加到程序中,而不会使代码充斥着与核心功能无关的内容。 https://en.wikipedia.org/wiki/Aspect-oriented_programming
这个概念很简单,可以用一句话来概括。
不写代码就能完成事情。
这适用于所有必需但又不引入任何业务逻辑的代码。
下面是一些 AOP 如何改变我们代码的示例。第一个是关于日志记录。
public void SaveData(InputClass input)
{
Stopwatch timer= new Stopwatch();
timer.Start();
logger.Info("enterd SaveData method");
if(logger.LogLevel==LoggingLeve.Debug)
{
logger.Debug(MyJsonTool.ConvertToJson(input);
}
dataRepositoryInstance.Save(input);
timer.End();
logger.Info($"enterd SaveData method in {timer.ElapsedMilliseconds}ms");
}
如果我告诉你,所有这些代码都可以通过只写这个来产生相同的输出,你会有什么想法?
public virtual void SaveData(InputClass input)
{
dataRepositoryInstance.Save(input);
}
所以,只需给方法添加一个 `virtual` 关键字就能完成所有工作,这太棒了!我们稍后会回到 `virtual` 关键字,以了解它与 AOP 的关系。
如果你不相信 AOP 的强大,那就看看数据获取的代码可以简化成这样
[Fetch("SELECT * FROM customers WHERE name=?")]
public List<MyDTO> GetByName(string name)
{
return new List<MyDTO>();
}
我希望现在你已经确信 AOP 可以在许多场景下提供帮助,并且是一个强大的盟友。让我们看看它是如何工作的,以及如何将其集成到 .NET Core 应用程序中。
本文包含两个示例
- 使用 DinamyProxy 和 Autofact 拦截日志的简单案例
- 对 AOP 技术进行一次深入的探讨,展示如何实现一个 AOP 引擎
示例 1:自动控制器日志记录
在这个示例中,我们将配置一个拦截器来记录所有传入的请求。这可以扩展到我们应用程序的所有其他层,这里仅作为概念验证。
拦截器
拦截器的结构非常简单。在方法执行之前和之后进行日志记录。在这个示例中,我使用 GUID 来关联事件日志,但还可以进行许多改进。
public class AutofacModule : Module
{
protected override void Load(ContainerBuilder builder)
{
// Register the interceptor
builder.Register(c => new CallLogger())
.Named<IInterceptor>("log-calls");
builder.Register(c => new ValuesService(c.Resolve<ILogger<ValuesService>>()))
.As<IValuesService>()
.InstancePerLifetimeScope()
.EnableInterfaceInterceptors();
}
}
public class CallLogger : IInterceptor
{
public void Intercept(IInvocation invocation)
{
var executionId = Guid.NewGuid().ToString();
Debug.WriteLine("{0} - Calling method {1} with parameters {2}... ",
executionId,
invocation.Method.Name,
JsonConvert.SerializeObject(invocation.Arguments));
invocation.Proceed();
Debug.WriteLine("{0} - Done: result was {1}.",
executionId,
JsonConvert.SerializeObject( invocation.ReturnValue));
}
}
我们可以讨论到明天,关于将数据转储到日志中的愚蠢之处,或者我们可以改进这个系统,使用更好的日志记录系统和一种巧妙的方法来跟踪输入、时间和输出。
正如你所见,这里有一个方法执行的跟踪,带有时间信息。想象一下,开箱即用,在你所有的 ASP.NET Web API 应用程序的控制器中,或者在你业务逻辑的每个服务方法中。很棒吧?这可以节省大量的代码行。
示例 2:低代码查询实现
这个示例展示了如何通过添加一些注解来为方法添加默认行为。这个示例是从零开始实现的,不使用任何库,以突出其幕后工作原理。
要创建的基类是 `DispatcherProxy`。这个类实现了泛型类型的代理,它拦截方法调用并返回一个自定义对象。这就是我们需要用一个可工作的对象来替换一个空方法。
无论如何,要实现一个通用的引擎,我们需要更多。我创建了一个通用的属性,叫做 `AOPAttribute`,里面有很多创意。所有继承它的注解都需要实现 `Execute` 方法。使用这种模式,所有的实现都委托给了注解,而我们的代理引擎与许多实现完全解耦。
你可以在下面的代码片段中查看代码的相关部分。只用几行代码,我们就实现了一个非常强大的引擎,但这只是一个例子。你可以尽情想象有多少用例可以为你解决。
我讲得太快了吗?让我们一步一步来。
步骤 1:我们想要什么
首先,我们想实现一个允许自动实现方法的机制。在 C# 中,我们不能在类上使用 `DispatcherProxy`,只能在接口上使用,所以我们总是需要从一个包含所有方法声明的接口开始。但是,我们也想手动实现一些方法,所以我们也需要一个具体的类。现在是棘手的部分。如果让类继承接口,这是很自然的,我们将被迫实现所有方法,因为编译器是这样要求的。我采用的技巧是根本不考虑继承。类和接口之间的关系将在以后,在 DI 期间定义。
这是 `FruitRepository` 的代码片段。接口包含将自动实现的方法,而 `InitDB` 是手动实现的方法。
public interface IFruitRepository
{
[Query("SELECT * FROM fruits where family={0}")]
List<Fruit> GetFruits(string tree);
[Query("SELECT * FROM fruits where family='Citrus'")]
List<Fruit> GetCitrus(string tree);
public void InitDB();
}
public class FruitRepositoryImpl
{
public void InitDB()
{
using (var db = new FruitDB())
{
db.Database.EnsureCreated();
var count = db.Fruits.Count();
if (count == 0)
{
db.Fruits.Add(new Fruit()
{
Name = "Lemon",
Family = "Citrus"
});
//... all the fruit of the world here
db.SaveChanges();
}
}
}
}
步骤 2:代理
现在我们需要创建一个代理,它将维护接口和实现类之间的关系,并根据注解提供方法。代码非常简单,请看下面的代码片段。
public class ProxyFacotory<T> : DispatchProxy
{
private T _decorated;
protected override object Invoke(MethodInfo targetMethod, object[] args)
{
//Find an annotation on the interface
var annotations = targetMethod.GetCustomAttributes(true);
var aopAttr = annotations.FirstOrDefault
(x => typeof(AOPAttribute).IsAssignableFrom(x.GetType())) as AOPAttribute;
//in case the method has an AOP implementation, this is executed
if (aopAttr != null)
{
return aopAttr.Execute(targetMethod, args, annotations);
}
//otherwise, the manual implementation on class is triggered
var inherithedMethod=interfaceMethods.FirstOrDefault
(x => x.Name == targetMethod.Name);
var result = inherithedMethod.Invoke(_decorated, args);
return result;
}
public static T Create<T,TProxy>(TProxy instance) where T : class where TProxy:class
{
object proxy = Create<T, ProxyFacotory<TProxy>>();
((ProxyFacotory<TProxy>)proxy).SetParameters(instance);
return (T)proxy;
}
private void SetParameters(T decorated)
{
_decorated = decorated;
}
}
用法非常简单,并且使用了常规的 .NET Core 依赖注入。
//the proxy instance return an instance based on the concrete implementation
var instance=ProxyFacotory<IFruitRepository>.Create<IFruitRepository,FruitRepositoryImpl>
(new FruitRepositoryImpl());
var serviceProvider = new ServiceCollection()
.AddSingleton<IFruitRepository>(instance)
.BuildServiceProvider();
步骤 3:注解
所有注解的基础是 `AOPAnnotation`。这是一个 `abstract` 类,包含一个 `Execute` 方法,该方法取代了常规的方法体。然后我们有 Query 注解,在我们的例子中,它使用开发人员传递的查询模板来获取数据。
public abstract class AOPAttribute: Attribute
{
public abstract object Execute
(MethodInfo targetMethod, object[] args, object[] annotations);
}
public class QueryAttribute : AOPAttribute
{
public string Template { get; set; }
public QueryAttribute(string template)
{
this.Template = template;
}
public override object Execute
(MethodInfo targetMethod, object[] args, object[] annotations)
{
using (var data = new FruitDB())
{
return data.Fruits
.FromSqlRaw<Fruit>(this.Template,args ).ToList(); //Dataset can be
//taken by target field
}
}
}
步骤 4:看它如何工作
将所有这些放在一起非常简单。只需像我们手动编写的那样使用存储库。
var fruitRepository = serviceProvider.GetService<IFruitRepository>();
//This calls the manual method and fill the database
fruitRepository.InitDB();
//This uses a dynamic definition
var fruits=fruitRepository.GetFruits("Citrus");
总结
AOP 是一个非常有趣的模式,因为它能够自动化代码编写。它非常强大,但有两个弱点
- 性能:大量使用反射和额外的处理步骤,可能会增加计算时间
- 失控:系统为你做的越多,你就越不知道如何修复
现代工具和框架有助于在不使用的情况下减少代码,所以它并不总是必要的。然而,当你设计一个框架或大型基础设施时,了解它非常重要,因为它可能是赢得战争的正确武器。例如,在我设计 RawCMS,开源无头 CMS 的架构时,它是一个很好的盟友。
关于性能或稳定性,只需记住 Java Spring Framework。它将其作为一切的基础,并且如今是企业应用程序的最佳选择之一。
参考文献
- AOP 的用例 [https://github.com/arduosoft/RawCMS]
- 一篇关于 .NET AOP 的精彩文章 [https://medium.com/@nik96a/using-di-with-dispatchproxy-based-decorators-in-c-net-core-ac02f02c5fe5]
- 维基百科上的 AOP 定义 [https://en.wikipedia.org/wiki/Aspect-oriented_programming]
- 本文的源代码 [https://github.com/zeppaman/csharp-aop]
- 最初由我在 medium 上发布 [https://levelup.gitconnected.com/aop-dotnet-applications-67c6d94c08b0]
历史
- 2020年5月12日:初版