首页 > 解决方案 > 使用 Postsharp 在运行时更改属性类变量

问题描述

我知道过去有人就围绕这个主题的类似问题提出了类似的问题,但没有一个回答我对现代和工作 C# 的担忧。

就我而言,我正在尝试为我的类变量实现“延迟缓存”,因为我们使用的 API 使我们能够同时请求特定变量,因此我们将它们分组为小字符集以方便(并降低数量对 API 的请求)。

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

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

这是一个 MVE:

懒加载.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;
            }
        }
    }

主文件

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);
    }

标签: c#runtimepostsharp

解决方案


诸如此类的自定义属性CatalogueLazyLoad基本上是在构建时与您的属性相关联的元数据。您不能在运行时修改其字段的值。

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

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

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

[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>());
    }
}

推荐阅读