said 属性 标记属性时是否可以拦截属性 的get 方法?

Is it possible to intercept the get method of a property when said property is marked with an attribute?

前言

我正在使用 Selenium 到 运行 自动化测试,Autofac 在 运行 时管理依赖项注入。

Selenium 使用一个 WebDriver 来控制浏览器,通常通过接口 IWebDriver. To find an element in a page, one can use the method FindElement which will return an object with the interface IWebElement. To find more than one element, we can use FindElements 反过来 return 一个 ReadOnlyCollection<IWebElement>

Web 自动化中最常用的模式是 Page Object Model,它建立了:

A page object is an object-oriented class that serves as an interface to a page of your AUT. The tests then use the methods of this page object class whenever they need to interact with the UI of that page. The benefit is that if the UI changes for the page, the tests themselves don’t need to change, only the code within the page object needs to change. Subsequently all changes to support that new UI are located in one place.

所以这方面的一个例子可以是:

public class SomePage
{
    private IWebDriver driver;
    private IWebElement UsernameElement => driver.FindElement(By.Id("username"));
    private IWebElement PasswordElement => driver.FindElement(By.Id("password"));
    private IWebElement LoginButton => driver.FindElement(By.Id("login"));

    public SomePage(IWebDriver driver)
    {
        this.driver = driver;
    }

    public Input_Username(string username) => UsernameElement.SendKeys(username);
    public Input_Password(string password) => PasswordElement.SendKeys(password);
    public Click_Login() => LoginButton.Click();
}

问题

最近一直在纠结一个问题:如果网页的某个元素是不可变的(意思是一旦在DOM中设置,就永远不会改变),我想保存的值第一次访问该元素时,return 每次我们需要该元素时。

前一个示例的第一种方法可能是:

public class SomePage
{
    ...
    private IWebElement usernameBackingField;
    private IWebElement UsernameElement
    {
        get
        {
            if (usernameBackingField == null)
                usernameBackingField = driver.FindElement(By.Id("username"));
         
            return usernameBackingField;
        }
    }
    ...
}

这样,我们第一次使用 属性 UsernameElement 时,我们将值保存在 usernameBackingField 中,每次我们需要这个元素时,我们都会保存它。

如果元素特别难创建,我们可以使用Lazy<IWebElement>保存值,稍后使用Value:

public class SomePage
{
    ...
    private Lazy<IWebElement> lazyUsername;
    private IWebElement UsernameElement
    {
        get
        {
            if (lazyUsername == null || lazyUsername.IsValueCreated == false)
                lazyUsername = new Lazy<IWebElement>(() => driver.FindElement(By.Id("username")), LazyThreadSafetyMode.PublicationOnly);
         
            return lazyUsername.Value;
        }
    }
    ...
}

此示例按预期工作,但对每个元素执行此操作很痛苦。

问题

知道我需要什么,那就是:拦截 属性 的 get method,将其用作 Lazy<IWebElement> 实例化的 Func<IWebElement> 和后来的 return 每次访问 属性 时输入 Lazy.Value,这是否是 Autofac 可以帮助我实现的,所以像下面这样的代码可以工作吗?

[Cacheable]
private IWebElement UsernameElement => driver.FindElement(By.Id("username"));

感谢您的宝贵时间。

Autofac 是一个依赖注入工具,这意味着它只会帮助获取实例及其所有依赖图。它不会帮助拦截方法或属性。

你需要的是一个Aspect Orient Programming tool. Such tools will help intercepting method and so on. One of the most popular one for .net is castle dynamic proxy

DynamicProxy 会自动为您的类型注入代理,并在需要的地方注入 IInterceptor

假设您有以下 class

public class Test1
{
    [Cacheable]
    protected virtual String UserName => "test";
}

DynamicProxy 将自动创建一个从 Test1 派生的 class 并覆盖 UserName 属性以注入这些 IInterceptor。这就是属性应该是虚拟的原因。

然后,您将使用动态代理给定的类型,而不是使用 Test1

获取代理类型的方法有很多种,其中一种解决方案是以下一种:

ProxyGenerator generator = new ProxyGenerator();
IInterceptor interceptor = new CacheableInterceptor();

Test1 test = new Test1();
Test1 proxy = generator.CreateClassProxyWithTarget<Test1>(test, interceptor);

在这种情况下,每次您使用 proxy 执行某些操作时,都会调用 CacheableInterceptor

CacheableInterceptor 的一个非常简单的实现可以是

public class CacheableInterceptor : IInterceptor
{
    private readonly Dictionary<String, String> _cache = new Dictionary<String, String>();

    public void Intercept(IInvocation invocation)
    {
        // This code is a sample and should not be used in production
        // performance optimization are available and is not thread safe

        var pi = invocation.Method
                           .DeclaringType
                           .GetProperties()
                           .Where(p => p.GetCustomAttributes<CacheableAttribute>().Any())
                           .FirstOrDefault(p => p.GetGetMethod() == invocation.Method);
        if (pi != null)
        {

            if (this._cache.TryGetValue(pi.Name, out var value))
            {
                invocation.ReturnValue = value;
            }
            else
            {
                invocation.Proceed();
                value = (String)invocation.ReturnValue;
                this._cache.Add(pi.Name, value);
            }
        }
    }
}

当您完全理解动态代理的工作原理时。我建议看看 Autofac 是如何工作的(没有拦截等),然后将 AutofacDynamicProxy 结合起来,让奇迹发生: autofac 可以为您创建代理、拦截器:参见 Autofac - Type interceptors