目錄
介紹
使用者故事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<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并在我們每次建立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>(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>();
}
}
您可以看到在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是原始類的擴充,是以可以用代理對象代替模型對象,例如,當資料通路層從資料庫讀取資料并将代理對象(而不是原始對象)傳回給調用者時,然後您将能夠跟蹤傳回的代理對象發生的所有更改。
我們還考慮了使用單元測試來檢查所建立的類是否按預期工作并調試我們的代碼。