使用 Postsharp 在运行时更改属性 class 变量

Change attribute class variable at runtime using Postsharp

我知道过去有人问过关于此主题的类似问题的类似问题,但其中 none 回答了我对现代和工作 C# 的担忧。

在我的例子中,我正在尝试为我的 class 变量实现一个 "lazy cache",因为我们使用的 API 使我们能够同时请求特定的变量,所以为了方便起见,我们将它们分组为小字符集(并减少对 API 的请求数)。

我正在使用 PostSharp 来实现这样的事情,使用 LocationInterceptionAspect 并重载每个缓存属性的 getter。我将我的属性添加到我的变量上方以告知它们在哪个字符集中。我们程序中使用的第一个变量应该为相同字符集中的其他变量加载值并告知它们已加载。

例如,假设我有 4 个具有相同字符集 "TEST_CHARSET" 的变量 a b c d。如果我这样做 Console.WriteLine(myObject.a) 这应该调用 API 来获取 "TEST_CHARSET" 字符集并填写其他变量值。一旦我调用 Console.WriteLine(myObject.b),就不应再调用 API,因为该值已经从上一次调用中收集了。

这是一个 MVE :

LazyLoad.cs

[PSerializable]
    [MulticastAttributeUsage(PersistMetaData = true, AllowExternalAssemblies = false)]
    [LinesOfCodeAvoided(50)]
    public sealed class CatalogueLazyLoad : LocationInterceptionAspect
    {
        #region PROPERTIES
        public string Name { get; set; }

        public string Charset { get; set; }

        public CacheType Cache { get; set; }

        public bool Loaded { get; set; } = false;
        #endregion

        public CatalogueLazyLoad(string name, string charset)
        {
            Name = name;
            Charset = charset;
            Cache = CacheType.CACHED;
        }

        private void GetValue(LocationInterceptionArgs args, bool propagate = false)
        {
            var properties = args.Instance.GetType().GetProperties();
            // JSONObject is just an object with string KEY and string VALUE, you can add dummy data here using a Dictionary<string, string>
            IEnumerable<JSONObject> result = API.Methods.GetCharsetData(id, Charset).Result;
            if (result.Count() > 0)
            {
                foreach (PropertyInfo propertyInfo in properties)
                {
                    CatalogueLazyLoad attribute = propertyInfo.GetCustomAttribute<CatalogueLazyLoad>();
                    if (attribute != null && attribute.Charset == Charset)
                    {
                        propertyInfo.SetValue(args.Instance, Convert.ChangeType(result.Where(x => x.Key == attribute.Name).Select(x => x.Value).FirstOrDefault(),
                            propertyInfo.PropertyType, CultureInfo.CurrentCulture), null);
                        if (propagate)
                        {
                            // THIS IS WHERE I AM STUCK, HOW TO SET true to LOADED of OTHERS ATTRIBUTES ??
                            propertyInfo.GetCustomAttribute<CatalogueLazyLoad>().Loaded = true;
                        }
                    }
                }
                args.ProceedGetValue();
            }
        }

        public override sealed void OnGetValue(LocationInterceptionArgs args)
        {
            base.OnGetValue(args);

            switch (Cache)
            {
                case CacheType.CACHED:
                    if (!Loaded)
                    {
                        GetValue(args, true);
                        Loaded = true;
                    }
                    break;
                case CacheType.FORCE_NO_CACHE:
                    GetValue(args);
                    break;
                default:
                    break;
            }
        }
    }

Main.cs

public class Test
    {
        [CatalogueLazyLoad("a", "TEST_CHARSET")]
        public string a { get; set; }

        [CatalogueLazyLoad("b", "TEST_CHARSET")]
        public string b { get; set; }

        [CatalogueLazyLoad("c", "TEST_CHARSET")]
        public string c { get; set; }

        [CatalogueLazyLoad("d", "TEST_CHARSET")]
        public string d { get; set; }
    }

    static void Main()
    {
        Test test = new Test();
        Console.WriteLine(test.a);
        // This should not call the API
        Console.WriteLine(test.b);
    }

CatalogueLazyLoad 等自定义属性基本上是在构建时与您的属性关联的元数据。您不能在 运行 时修改它们字段的值。

还有一个在 运行 时间为每个 属性 创建的方面实例(它也是 CatalogueLazyLoad 的一个实例)。但是这些无法通过反射 API 和 propertyInfo.GetCustomAttribute.

等方法访问

您需要的是一种在 CatalogueLazyLoad class 的多个实例之间共享一些数据的方法。对于此类用例,将自定义属性引入和导入目标 class 效果很好。我建议你在目标class中引入一个属性LoadedCharsets。 属性 将保留已加载的字符集的集合,所有方面实例都将访问同一集合实例。

下面的示例展示了如何在您的 CatalogueLazyLoad class 中实现这一点。它不处理多线程,因此您可能需要在需要时添加它。

[PSerializable]
[MulticastAttributeUsage(PersistMetaData = true, AllowExternalAssemblies = false)]
[LinesOfCodeAvoided(50)]
// We need to implement IInstanceScopedAspect to introduce and import members
public sealed class CatalogueLazyLoad : LocationInterceptionAspect, IInstanceScopedAspect
{
    public string Name { get; set; }

    public string Charset { get; set; }

    public CacheType Cache { get; set; }

    // Introduce a new property into the target class (only once)
    [IntroduceMember(OverrideAction = MemberOverrideAction.Ignore)]
    public HashSet<string> LoadedCharsets { get; set; }

    // Import the introduced property (it may be introduced by this aspect or another aspect on another property)
    [ImportMember("LoadedCharsets", IsRequired = true, Order = ImportMemberOrder.AfterIntroductions)]
    public Property<HashSet<string>> LoadedCharsetsProperty;

    public CatalogueLazyLoad(string name, string charset)
    {
        Name = name;
        Charset = charset;
        Cache = CacheType.CACHED;
    }

    private void GetValue(LocationInterceptionArgs args, bool propagate = false)
    {
        var properties = args.Instance.GetType().GetProperties();
        // JSONObject is just an object with string KEY and string VALUE, you can add dummy data here using a Dictionary<string, string>
        IEnumerable<JSONObject> result = API.Methods.GetCharsetData(id, Charset).Result;
        if (result.Count() > 0)
        {
            foreach (PropertyInfo propertyInfo in properties)
            {
                CatalogueLazyLoad attribute = propertyInfo.GetCustomAttribute<CatalogueLazyLoad>();
                if (attribute != null && attribute.Charset == Charset)
                {
                    propertyInfo.SetValue(args.Instance,
                                          Convert.ChangeType(result.Where(x => x.Key == attribute.Name).Select(x => x.Value).FirstOrDefault(), propertyInfo.PropertyType, CultureInfo.CurrentCulture),
                                          null);
                }
            }

            if (propagate)
            {
                this.LoadedCharsetsProperty.Get().Add(this.Charset);
            }

            args.ProceedGetValue();
        }
    }

    public override sealed void OnGetValue(LocationInterceptionArgs args)
    {
        base.OnGetValue(args);

        switch (Cache)
        {
            case CacheType.CACHED:
                bool loaded = this.LoadedCharsetsProperty.Get().Contains(this.Charset);
                if (!loaded)
                {
                    GetValue(args, true);
                }
                break;
            case CacheType.FORCE_NO_CACHE:
                GetValue(args);
                break;
            default:
                break;
        }
    }

    public object CreateInstance(AdviceArgs adviceArgs)
    {
        return this.MemberwiseClone();
    }

    public void RuntimeInitializeInstance()
    {
        this.LoadedCharsetsProperty.Set(new HashSet<string>());
    }
}