目录
介绍
用户故事3:拦截模型调用
实现——模型
实现——代理工厂
实现——单元测试
实现——规则引擎
总结
- 从Github下载完整的解决方案
我想向您展示一个强大的开源库,称为Castle DynamicProxy,它使您能够使用代理来拦截对模型类的调用。代理是在运行时动态生成的,因此您无需更改模型类即可开始拦截其属性或方法。
顺便说一下,在模型类中包含方法和任何其他逻辑不是一个好的设计决定。
让我首先定义一个用户故事。
用户故事3:拦截模型调用- 添加跟踪模型类中的更改的功能
- 将调用序列存储在附加到模型类的集合中
让我们创建一个新的Visual Studio项目,但是这次让我们使用.NET Core类库,并使用xUnit测试框架检查我们的代码如何工作。我们添加一个新的主项目并命名为DemoCastleProxy:
创建主项目后,将新的xUnit项目DemoCastleProxyTests添加到解决方案中,我们将需要它来检查代理演示的工作方式:
我们的用户故事说,我们需要在模型类中有一个用于跟踪更改的集合,因此我们从定义此集合的接口开始。如果要创建更多的模型类,则可以重用此接口。让我们向主项目添加一个新接口:
using System;
using System.Collections.Generic;
using System.Text;
namespace DemoCastleProxy
{
public interface IModel
{
List 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 PropertyChangeList { get; set; } =
new List();
}
}
如您所见,PersonModel实现接口IModel并在我们每次创建PersonModel的实例时进行初始化PropertyChangeList。您还可以看到我使用virtual关键字标记了所有属性。这是模型定义的重要组成部分。
Castle DynamicProxy仅可以拦截虚拟属性,并使用多态来实现。实际上,Castle代理引擎通过从模型类创建继承的类以这种方式工作,并且它覆盖了所有虚拟属性。当您调用overridden属性时,它将首先执行一个拦截器,然后才将其移交给基本模型类。
您可以尝试通过手动创建代理来自己执行此操作。它可能看起来像这样:
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设计的文章,尤其是阅读《四种设计模式的帮派》一书。
我会用另一种设计模式,Factory Method以实现通用逻辑的代理生成。
但是在此之前,我们需要将Castle.Core NuGet包添加到主项目中:
现在,我将从一个接口开始:
using System;
using System.Collections.Generic;
using System.Text;
namespace DemoCastleProxy
{
public interface IProxyFactory
{
T GetModelProxy(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 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 PropertyChangeList { get; set; } =
new List();
}
}
您可以看到在LastName属性上方添加的[ModelRule(typeof(PersonRule))]特性,并且我们向.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:
- https://github.com/euklad/BlogCode
今天,我们讨论了Castle开源库,该库可用于动态代理生成以及侦听对代理方法和属性的调用。
因为proxy是原始类的扩展,所以可以用代理对象代替模型对象,例如,当数据访问层从数据库读取数据并将代理对象(而不是原始对象)返回给调用者时,然后您将能够跟踪返回的代理对象发生的所有更改。
我们还考虑了使用单元测试来检查所创建的类是否按预期工作并调试我们的代码。