Change attribute class variable at runtime using Postsharp


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.


LocationInterceptionAspect



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.


a b c d


"TEST_CHARSET"


Console.WriteLine(myObject.a)


"TEST_CHARSET"


Console.WriteLine(myObject.b)



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




1 Answer
1



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.


CatalogueLazyLoad



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.


CatalogueLazyLoad


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.


CatalogueLazyLoad


LoadedCharsets



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.


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





I was using a static class containing a static list of charsets to keep record of them but your method looks way cleaner. I'll test it tomorrow and accept your answer asap if it fixes my issue. Thanks !
– rak007
Jul 1 at 21:16





This is working like a charm. Thank you for the time you took to answer and thank you for developing this awesome tool that is PostSharp !
– rak007
Jul 2 at 8:09





You're welcome! I've just noticed my copy-paste mistake and fixed the naming of the property :)
– AlexD
Jul 2 at 8:15





I did fixed it on my own haha
– rak007
Jul 2 at 8:17






By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Popular posts from this blog

How to add background colour in existing image using Swift?

Moria Casán

How to make file upload 'Required' in Contact Form 7?