Castle Dynamic Proxy Interceptors 跟踪模型更改并触发规则
添加跟踪模型类更改的功能;使用代理拦截器执行附加到模型属性的规则
引言
大家好,欢迎阅读我的新文章。
我想向您介绍一个强大的开源库,名为 Castle DynamicProxy,它使您能够使用代理拦截对模型类的调用。代理在运行时动态生成,因此您无需更改模型类即可开始拦截其属性或方法。
顺便说一句,在模型类中包含方法和任何其他逻辑并不是一个好的设计决策。
我将从定义一个用户故事开始。
用户故事 #3:拦截模型调用
- 添加跟踪模型类中更改的功能
- 将一系列调用存储在附加到模型类的集合中
实现 - 模型
让我们创建一个新的 Visual Studio 项目,这次我们使用 .NET Core 类库,并使用 xUnit 测试框架来检查我们的代码是如何工作的。我们添加一个新的主项目,并将其命名为 DemoCastleProxy
。
创建主项目后,向解决方案中添加一个名为 DemoCastleProxyTests
的 xUnit 项目,我们需要它来检查我们的代理演示是如何工作的。
我们的用户故事说我们需要在模型类中有一个用于跟踪更改的集合,所以我们从一个定义该集合的接口开始。如果我们想创建更多的模型类,我们可以重用这个接口。让我们在主项目中添加一个新的接口。
using System;
using System.Collections.Generic;
using System.Text;
namespace DemoCastleProxy
{
public interface IModel
{
List<string> PropertyChangeList { get; }
}
}
现在我们可以添加模型类了。
using System;
using System.Collections.Generic;
using System.Text;
namespace DemoCastleProxy
{
public class PersonModel : IModel
{
public virtual string FirstName { get; set; }
public virtual string LastName { get; set; }
public virtual DateTime? BirthDate { get; set; }
public virtual List<string> PropertyChangeList { get; set; } =
new List<string>();
}
}
正如您所见,PersonModel
实现了接口 IModel
,并且 PropertyChangeList
在每次创建 PersonModel
实例时都会被初始化。您还可以看到,我使用 virtual
关键字标记了所有属性。这是模型定义的重要部分。
Castle DynamicProxy 只能拦截虚属性,通过多态来实现这一点。实际上,Castle 代理引擎通过创建继承自您的模型类的子类来工作,并重写所有虚属性。当您调用被重写的属性时,它首先执行拦截器,然后才将您的调用转交给基模型类。
您可以尝试自己手动创建一个代理。它可能看起来像这样
public class PersonModelProxy : PersonModel
{
public override string FirstName
{
get
{
Intercept("get_FirstName", base.FirstName);
return base.FirstName;
}
set
{
Intercept("set_FirstName", value);
base.FirstName = value;
}
}
private void Intercept(string propertyName, object value)
{
// do something here
}
}
但 Castle 会在运行时以通用方式为我们完成这项工作,并且代理类将拥有与原始模型类相同的属性 - 因此我们只需要维护我们的模型类。
实现 - 代理工厂
你们很多人都知道 Proxy
是一个结构性设计模式,我一直建议开发人员阅读有关 OOP 设计的内容,特别是阅读《设计模式》(Gang of Four Design Patterns)一书。
我将使用另一个设计模式,即工厂方法(Factory Method)
,来实现代理生成的通用逻辑。
但在所有这些之前,我们需要将 Castle.Core
NuGet 包添加到主项目中。
现在,我将从一个接口开始。
using System;
using System.Collections.Generic;
using System.Text;
namespace DemoCastleProxy
{
public interface IProxyFactory
{
T GetModelProxy<T>(T source) where T : class;
}
}
并添加其实现。
using Castle.DynamicProxy;
using System;
namespace DemoCastleProxy
{
public class ProxyFactory : IProxyFactory
{
private readonly IProxyGenerator _proxyGenerator;
private readonly IInterceptor _interceptor;
public ProxyFactory(IProxyGenerator proxyGenerator, IInterceptor interceptor)
{
_proxyGenerator = proxyGenerator;
_interceptor = interceptor;
}
public T GetModelProxy<T>(T source) where T : class
{
var proxy = _proxyGenerator.CreateClassProxyWithTarget(source.GetType(),
source, new IInterceptor[] { _interceptor }) as T;
return proxy;
}
}
}
使用接口使我们能够拥有 IProxyFactory
的多个实现,并在启动时的依赖注入注册中选择其中一个。
我们使用 Castle 框架中的 CreateClassProxyWithTarget
方法,从提供的模型对象创建代理对象。
现在我们需要实现一个拦截器,该拦截器将被传递给 ProxyFactory
构造函数,并作为第二个参数提供给 CreateClassProxyWithTarget
方法。
拦截器的代码将是:
using Castle.DynamicProxy;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
namespace DemoCastleProxy
{
public class ModelInterceptor : IInterceptor
{
public void Intercept(IInvocation invocation)
{
invocation.Proceed();
var method = invocation.Method.Name;
if (method.StartsWith("set_"))
{
var field = method.Replace("set_", "");
var proxy = invocation.Proxy as IModel;
if (proxy != null)
{
proxy.PropertyChangeList.Add(field);
}
}
}
}
}
对代理对象的每次调用都会执行 Intercept
方法。在此方法中,我们检查是否调用了属性设置器,如果是,则将调用属性的名称添加到 PropertyChangeList
中。
现在我们可以编译我们的代码了。
实现 - 单元测试
我们需要运行我们的代码以确保其正常工作,其中一种可能的方法是创建一个单元测试。这比创建一个使用我们代理的应用程序要快得多。
在 Pro Coders,我们非常重视单元测试,因为经过单元测试的代码也能在应用程序中正常工作。此外,如果您重构了受单元测试覆盖的代码,您可以确信重构后,如果单元测试通过,您的代码将正常工作。
让我们添加第一个测试。
using Castle.DynamicProxy;
using DemoCastleProxy;
using System;
using Xunit;
namespace DemoCastleProxyTests
{
public class DemoTests
{
private IProxyFactory _factory;
public DemoTests()
{
_factory = new ProxyFactory(new ProxyGenerator(), new ModelInterceptor());
}
[Fact]
public void ModelChangesInterceptedTest()
{
PersonModel model = new PersonModel();
PersonModel proxy = _factory.GetModelProxy(model);
proxy.FirstName = "John";
Assert.Single(model.PropertyChangeList);
Assert.Single(proxy.PropertyChangeList);
Assert.Equal("FirstName", model.PropertyChangeList[0]);
Assert.Equal("FirstName", proxy.PropertyChangeList[0]);
}
}
}
在 xUnit 中,我们需要用 [Fact]
属性标记每个测试方法。
在 DemoTests
构造函数中,我创建了 _factory
并将 ModelInterceptor
的新实例作为参数提供,尽管在应用程序中,我们将使用依赖注入来实例化 ProxyFactory
。
现在,在我们的测试类的每个方法中,我们都可以使用 _factory
来创建代理对象。
我的测试只是创建一个新的模型对象,然后从模型生成一个代理对象。现在对代理对象的任何调用都应该被拦截,并且 PropertyChangeList
将被填充。
要运行您的单元测试,请将光标放在测试方法体内的任何位置,然后单击 [Ctrl+R]+[Ctrl+T]。如果快捷键无效,请使用上下文菜单或测试资源管理器窗口。
如果设置断点,您可以看到我们使用的变量的值。
如您所见,我们更改了 FirstName
属性,它已出现在 PropertyChangeList
中。
实现 - 规则引擎
让我们使这个练习更有趣,并使用我们的拦截器来执行附加到模型属性的规则。
我们将使用 C# 属性来附加拦截器应执行的规则类型,让我们创建它。
using System;
using System.Collections.Generic;
using System.Text;
namespace DemoCastleProxy
{
public class ModelRuleAttribute : Attribute
{
public Type Rule { get; private set; }
public ModelRuleAttribute(Type rule)
{
Rule = rule;
}
}
}
拥有属性的名称允许拦截器使用反射读取附加到属性的属性,并执行规则。
为了使其优雅,我们将定义 IModelRule
接口。
using System;
using System.Collections.Generic;
using System.Text;
namespace DemoCastleProxy
{
public interface IModelRule
{
void Execute(object model, string fieldName);
}
}
而我们的规则将像这样实现它:
using System;
using System.Collections.Generic;
using System.Text;
namespace DemoCastleProxy
{
public class PersonRule : IModelRule
{
public void Execute(object model, string fieldName)
{
var personModel = model as PersonModel;
if (personModel != null && fieldName == "LastName")
{
if (personModel.FirstName?.ToLower() == "john" &&
personModel.LastName?.ToLower() == "lennon")
{
personModel.BirthDate = new DateTime(1940, 10, 9);
}
}
}
}
}
规则将检查更改的字段是否为 LastName
(仅当执行 LastName
设置器时),并且 FirstName
和 LastName
是否具有不区分大小写的“John Lennon
”值,然后它将自动设置 BirthDate
字段。
现在我们需要将规则附加到我们的模型。
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text;
namespace DemoCastleProxy
{
public class PersonModel : IModel
{
public virtual string FirstName { get; set; }
[ModelRule(typeof(PersonRule))]
public virtual string LastName { get; set; }
public virtual DateTime? BirthDate { get; set; }
public virtual List<string> PropertyChangeList { get; set; } =
new List<string>();
}
}
您可以看到 [ModelRule(typeof(PersonRule))]
属性已添加到 LastName
属性上方,并且我们已将规则类型提供给 .NET。
我们还需要修改 ModelInterceptor
,添加执行规则的功能,新代码将添加到// rule execution
注释之后。
using Castle.DynamicProxy;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
namespace DemoCastleProxy
{
public class ModelInterceptor : IInterceptor
{
public void Intercept(IInvocation invocation)
{
invocation.Proceed();
var method = invocation.Method.Name;
if (method.StartsWith("set_"))
{
var field = method.Replace("set_", "");
var proxy = invocation.Proxy as IModel;
if (proxy != null)
{
proxy.PropertyChangeList.Add(field);
}
// rule execution
var model = ProxyUtil.GetUnproxiedInstance(proxy) as IModel;
var ruleAttribute = model.GetType().GetProperty(field).GetCustomAttribute
(typeof(ModelRuleAttribute)) as ModelRuleAttribute;
if (ruleAttribute != null)
{
var rule = Activator.CreateInstance(ruleAttribute.Rule) as IModelRule;
if (rule != null)
{
rule.Execute(invocation.Proxy, field);
}
}
}
}
}
}
拦截器通过反射简单地读取已触发属性的自定义属性,如果它找到附加到属性的规则,它将创建该规则实例并执行它。
现在最后一步是检查我们的规则引擎是否正常工作,让我们在 DemoTests
类中为它创建另一个单元测试。
[Fact]
public void ModelRuleExecutedTest()
{
var model = new PersonModel();
var proxy = _factory.GetModelProxy(model);
proxy.FirstName = "John";
Assert.NotEqual("1940-10-09", model.BirthDate?.ToString("yyyy-MM-dd"));
proxy.LastName = "Lennon";
Assert.Equal("1940-10-09", model.BirthDate?.ToString("yyyy-MM-dd"));
}
此测试将 FirstName
设置为“John
”,并检查 BirthDate
属性是否不是 1940-10-09,然后将 LastName
设置为“Lennon
”,并检查 BirthDate
现在是否为 1940-10-09。
我们可以运行它,确保拦截器执行了规则并更改了 BirthDate
的值。我们还可以使用调试器来查看设置 LastName
属性时发生的情况,我确信——这很有趣。
此外,进行负面测试也是一个好习惯——测试相反的情况。让我们创建一个测试,检查如果全名不是“John Lennon
”则什么都不会发生。
[Fact]
public void ModelRuleNotExecutedTest()
{
var model = new PersonModel();
var proxy = _factory.GetModelProxy(model);
proxy.FirstName = "John";
Assert.NotEqual("1940-10-09", model.BirthDate?.ToString("yyyy-MM-dd"));
proxy.LastName = "Travolta";
Assert.NotEqual("1940-10-09", model.BirthDate?.ToString("yyyy-MM-dd"));
}
您可以在我的 GitHub 上找到完整的解决方案代码,文件夹为 DemoCastleProxy-story3
。
将代码升级到 .NET 7.0 和 Castle.Core 5.1.1
由于读者提出了一些问题,我将本文的代码库升级到了最新版本的 .NET 和 Castle。在我看来,它按预期工作,我截了一张图。
如果您看底部,我高亮了代理对象的真实类型——它是“Castle.Proxies.PersonMoldeProxy
”。
摘要
今天,我们讨论了 Castle 开源库,它对于动态代理生成和拦截代理方法和属性的调用非常有用。
由于代理是原始类的扩展,您可以将模型对象替换为代理对象,例如,当您的数据访问层从数据库读取数据并将代理对象(而不是原始对象)返回给调用者时,然后您将能够跟踪返回的代理对象上发生的所有更改。
我们还考虑使用单元测试来检查创建的类是否按预期工作以及调试我们的代码。
感谢阅读!
历史
- 2020年10月24日:初始版本