Search code examples
c#runtimepostsharp

Change attribute class variable at runtime using Postsharp


I know similar questions were asked in the past about similar things arround this topic but none of them answered my concern with modern and working C#.

In my case, I am trying to implement a "lazy cache" for my class variables because the API we are using enables us to ask for specific variables at the same time so we group them in small charset for convience (and to lower the number of requests to the API).

I am using PostSharp to achieve such thing using the LocationInterceptionAspect and overloading the getter of each cached properties. I add my attribute above my variable to tell in which charset they are. The first variable to be used in our program should load values for others in the same charset and tell that they were loaded.

For instance, let's say I have 4 variables a b c d of the same charset "TEST_CHARSET". If I do Console.WriteLine(myObject.a) this should call the API to get the "TEST_CHARSET" charset and fill in the other variables values. Once I call Console.WriteLine(myObject.b), no calls to the API should be made as the value was already gathered from the previous call.

Here is a 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);
    }

Solution

  • The custom attributes such as CatalogueLazyLoad are basically metadata that are associated with your properties at build-time. You cannot modify the values of their fields at run-time.

    There's also an instance of the aspect created for each property at run-time (it's also an instance of CatalogueLazyLoad). But those cannot be access via reflection API and methods like propertyInfo.GetCustomAttribute.

    What you need is a way to share some data between many instances of the CatalogueLazyLoad class. For such use cases introducing and importing custom properties into the target class works well. I suggest that you introduce a property LoadedCharsets into the target class. This property will keep a collection of the charsets that are already loaded and the same collection instance will be accessed by all aspect instances.

    The sample below shows how to implement this in your CatalogueLazyLoad class. It doesn't handle multi-threading, so you may want to add that if needed.

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