From 5d82a73c9d80d529aea51405bb5b3f74c91280a5 Mon Sep 17 00:00:00 2001 From: David Ellams Date: Thu, 23 Mar 2023 21:08:59 -0500 Subject: [PATCH] - Added optional caching to SaveAsync & Save methods in HoloNETEntryBaseClass & HoloNETAuditEntryBaseClass so the property infos are cached when using refelction which may give a slight performance improvement. - Added optional caching to MapEntryDataObjectAsync & MapEntryDataObject methods in HoloNETClient so the property infos are cached when using refelction which may give a slight performance improvement. - Updated README.md/documentation. --- .../HoloNETClient.cs | 35 ++++++++++++++----- .../HoloNETAuditEntryBaseClass.cs | 17 ++++++--- .../HoloNETEntry/HoloNETEntryBaseClass.cs | 35 ++++++++++++------- .../Single Class Example/Avatar.cs | 26 +++++++------- README.md | 35 ++++++++++--------- 5 files changed, 94 insertions(+), 54 deletions(-) diff --git a/NextGenSoftware.Holochain.HoloNET.Client.Core/HoloNETClient.cs b/NextGenSoftware.Holochain.HoloNET.Client.Core/HoloNETClient.cs index 090fadb..32d6309 100644 --- a/NextGenSoftware.Holochain.HoloNET.Client.Core/HoloNETClient.cs +++ b/NextGenSoftware.Holochain.HoloNET.Client.Core/HoloNETClient.cs @@ -30,6 +30,7 @@ public class HoloNETClient //: IDisposable //, IAsyncDisposable private Dictionary> _taskCompletionZomeCallBack = new Dictionary>(); private TaskCompletionSource _taskCompletionAgentPubKeyAndDnaHashRetrieved = new TaskCompletionSource(); //private TaskCompletionSource _taskCompletionAgentPubKeyAndDnaHashRetrievedFromConductor = new TaskCompletionSource(); + private Dictionary _dictPropertyInfos = new Dictionary(); private TaskCompletionSource _taskCompletionReadyForZomeCalls = new TaskCompletionSource(); private TaskCompletionSource _taskCompletionDisconnected = new TaskCompletionSource(); private TaskCompletionSource _taskCompletionHoloNETShutdown = new TaskCompletionSource(); @@ -1453,10 +1454,11 @@ public string ConvertHoloHashToString(byte[] bytes) /// /// The type of the data object to map the KeyValuePairs returned from the Holochain Conductor onto. /// The KeyValuePairs returned from the Holochain Conductor (after they have been decoded by an internal function called `DecodeRawZomeData`) that will be mapped onto the data object. + /// Set this to true if you want HoloNET to cache the property info for the Entry Data Object passed in (this can reduce the slight overhead used by reflection). /// - public async Task MapEntryDataObjectAsync(Type entryDataObjectType, Dictionary keyValuePairs) + public async Task MapEntryDataObjectAsync(Type entryDataObjectType, Dictionary keyValuePairs, bool cacheEntryDataObjectPropertyInfo = true) { - return await MapEntryDataObjectAsync(Activator.CreateInstance(entryDataObjectType), keyValuePairs); + return await MapEntryDataObjectAsync(Activator.CreateInstance(entryDataObjectType), keyValuePairs, cacheEntryDataObjectPropertyInfo); } /// @@ -1464,10 +1466,11 @@ public async Task MapEntryDataObjectAsync(Type entryDataObjectType, Dic /// /// The type of the data object to map the KeyValuePairs returned from the Holochain Conductor onto. /// The KeyValuePairs returned from the Holochain Conductor (after they have been decoded by an internal function called `DecodeRawZomeData`) that will be mapped onto the data object. + /// Set this to true if you want HoloNET to cache the property info for the Entry Data Object passed in (this can reduce the slight overhead used by reflection). /// - public dynamic MapEntryDataObject(Type entryDataObjectType, Dictionary keyValuePairs) + public dynamic MapEntryDataObject(Type entryDataObjectType, Dictionary keyValuePairs, bool cacheEntryDataObjectPropertyInfo = true) { - return MapEntryDataObjectAsync(entryDataObjectType, keyValuePairs).Result; + return MapEntryDataObjectAsync(entryDataObjectType, keyValuePairs, cacheEntryDataObjectPropertyInfo).Result; } /// @@ -1475,14 +1478,29 @@ public dynamic MapEntryDataObject(Type entryDataObjectType, Dictionary /// The dynamic data object to map the KeyValuePairs returned from the Holochain Conductor onto. /// The KeyValuePairs returned from the Holochain Conductor (after they have been decoded by an internal function called `DecodeRawZomeData`) that will be mapped onto the data object. + /// Set this to true if you want HoloNET to cache the property info's for the Entry Data Object passed in (this can reduce the slight overhead used by reflection). /// - public async Task MapEntryDataObjectAsync(dynamic entryDataObject, Dictionary keyValuePairs) + public async Task MapEntryDataObjectAsync(dynamic entryDataObject, Dictionary keyValuePairs, bool cacheEntryDataObjectPropertyInfos = true) { try { + PropertyInfo[] props = null; + if (keyValuePairs != null && entryDataObject != null) { - PropertyInfo[] props = entryDataObject.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance); + Type type = entryDataObject.GetType(); + string typeKey = $"{type.AssemblyQualifiedName}.{type.FullName}"; + + if (cacheEntryDataObjectPropertyInfos && _dictPropertyInfos.ContainsKey(typeKey)) + props = _dictPropertyInfos[typeKey]; + else + { + //Cache the props to reduce overhead of reflection. + props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance); + + if (cacheEntryDataObjectPropertyInfos) + _dictPropertyInfos[typeKey] = props; + } foreach (PropertyInfo propInfo in props) { @@ -1569,10 +1587,11 @@ public async Task MapEntryDataObjectAsync(dynamic entryDataObject, Dict /// /// The dynamic data object to map the KeyValuePairs returned from the Holochain Conductor onto. /// The KeyValuePairs returned from the Holochain Conductor (after they have been decoded by an internal function called `DecodeRawZomeData`) that will be mapped onto the data object. + /// Set this to true if you want HoloNET to cache the property info's for the Entry Data Object passed in (this can reduce the slight overhead used by reflection). /// - public dynamic MapEntryDataObject(dynamic entryDataObject, Dictionary keyValuePairs) + public dynamic MapEntryDataObject(dynamic entryDataObject, Dictionary keyValuePairs, bool cacheEntryDataObjectPropertyInfos = true) { - return MapEntryDataObjectAsync(entryDataObject, keyValuePairs).Result; + return MapEntryDataObjectAsync(entryDataObject, keyValuePairs, cacheEntryDataObjectPropertyInfos).Result; } ///// diff --git a/NextGenSoftware.Holochain.HoloNET.Client.Core/HoloNETEntry/HoloNETAuditEntryBaseClass.cs b/NextGenSoftware.Holochain.HoloNET.Client.Core/HoloNETEntry/HoloNETAuditEntryBaseClass.cs index ff3ee50..f27815e 100644 --- a/NextGenSoftware.Holochain.HoloNET.Client.Core/HoloNETEntry/HoloNETAuditEntryBaseClass.cs +++ b/NextGenSoftware.Holochain.HoloNET.Client.Core/HoloNETEntry/HoloNETAuditEntryBaseClass.cs @@ -353,8 +353,9 @@ public HoloNETAuditEntryBaseClass(string zomeName, string zomeLoadEntryFunction, /// /// This is a optional dictionary containing keyvalue pairs of custom data you wish to inject into the params that are sent to the zome function. /// This is a optional dictionary containing keyvalue pairs to allow properties that contain the HolochainFieldName to be omitted from the data sent to the zome function. The key (case senstive) needs to match a property that has the HolochainFieldName attribute. + /// Set this to true if you want HoloNET to cache the property info's for the Entry Data Object (this can reduce the slight overhead used by reflection). /// - public override async Task SaveAsync(Dictionary customDataKeyValuePair = null, Dictionary holochainFieldsIsEnabledKeyValuePair = null) + public override async Task SaveAsync(Dictionary customDataKeyValuePair = null, Dictionary holochainFieldsIsEnabledKeyValuePair = null, bool cachePropertyInfos = true) { if (string.IsNullOrEmpty(EntryHash)) { @@ -394,12 +395,20 @@ public override async Task SaveAsync(Dictionary customDataKeyValuePair = null, Dictionary holochainFieldsIsEnabledKeyValuePair = null) + /// + /// Saves the object and will automatically extrct the properties that need saving (contain the HolochainFieldName attribute). This method uses reflection so has a tiny performance overhead (negligbale), but if you need the extra nanoseconds use the other Save overload passing in your own params object. + /// NOTE: This overload now also allows you to pass in your own params object but it will still dynamically add any properties that have the HolochainFieldName attribute. + /// + /// This is a optional dictionary containing keyvalue pairs of custom data you wish to inject into the params that are sent to the zome function. + /// This is a optional dictionary containing keyvalue pairs to allow properties that contain the HolochainFieldName to be omitted from the data sent to the zome function. The key (case senstive) needs to match a property that has the HolochainFieldName attribute. + /// Set this to true if you want HoloNET to cache the property info's for the Entry Data Object (this can reduce the slight overhead used by reflection). + /// + public override ZomeFunctionCallBackEventArgs Save(Dictionary customDataKeyValuePair = null, Dictionary holochainFieldsIsEnabledKeyValuePair = null, bool cachePropertyInfos = true) { - return SaveAsync(customDataKeyValuePair, holochainFieldsIsEnabledKeyValuePair).Result; + return SaveAsync(customDataKeyValuePair, holochainFieldsIsEnabledKeyValuePair, cachePropertyInfos).Result; } /// diff --git a/NextGenSoftware.Holochain.HoloNET.Client.Core/HoloNETEntry/HoloNETEntryBaseClass.cs b/NextGenSoftware.Holochain.HoloNET.Client.Core/HoloNETEntry/HoloNETEntryBaseClass.cs index f66b1fc..35a77cb 100644 --- a/NextGenSoftware.Holochain.HoloNET.Client.Core/HoloNETEntry/HoloNETEntryBaseClass.cs +++ b/NextGenSoftware.Holochain.HoloNET.Client.Core/HoloNETEntry/HoloNETEntryBaseClass.cs @@ -13,6 +13,7 @@ public abstract class HoloNETEntryBaseClass //: IDisposable { private Dictionary _holochainProperties = new Dictionary(); private bool _disposeOfHoloNETClient = false; + private PropertyInfo[] _propInfoCache = null; public delegate void Error(object sender, HoloNETErrorEventArgs e); @@ -516,21 +517,31 @@ public virtual ZomeFunctionCallBackEventArgs Load() /// /// This is a optional dictionary containing keyvalue pairs of custom data you wish to inject into the params that are sent to the zome function. /// This is a optional dictionary containing keyvalue pairs to allow properties that contain the HolochainFieldName to be omitted from the data sent to the zome function. The key (case senstive) needs to match a property that has the HolochainFieldName attribute. + /// Set this to true if you want HoloNET to cache the property info's for the Entry Data Object (this can reduce the slight overhead used by reflection). /// - public virtual async Task SaveAsync(Dictionary customDataKeyValuePair = null, Dictionary holochainFieldsIsEnabledKeyValuePair = null) + public virtual async Task SaveAsync(Dictionary customDataKeyValuePair = null, Dictionary holochainFieldsIsEnabledKeyValuePair = null, bool cachePropertyInfos = true) { try { + if (!IsInitialized && !IsInitializing) + await InitializeAsync(); + dynamic paramsObject = new ExpandoObject(); dynamic updateParamsObject = new ExpandoObject(); + PropertyInfo[] props = null; Dictionary zomeCallProps = new Dictionary(); + Type type = GetType(); - if (!IsInitialized && !IsInitializing) - await InitializeAsync(); - - //TODO: Add caching for the properties (key: full assembly and type, value: array of propertyInfo). - PropertyInfo[] props = GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance); - //bool update = false; + if (cachePropertyInfos && _propInfoCache != null) + props = _propInfoCache; + else + { + //Cache the props to reduce overhead of reflection. + props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance); + + if (cachePropertyInfos) + _propInfoCache = props; + } foreach (PropertyInfo propInfo in props) { @@ -611,13 +622,13 @@ public virtual async Task SaveAsync(Dictionary + /// This is a optional dictionary containing keyvalue pairs of custom data you wish to inject into the params that are sent to the zome function. + /// This is a optional dictionary containing keyvalue pairs to allow properties that contain the HolochainFieldName to be omitted from the data sent to the zome function. The key (case senstive) needs to match a property that has the HolochainFieldName attribute. + /// Set this to true if you want HoloNET to cache the property info's for the Entry Data Object (this can reduce the slight overhead used by reflection). /// - public virtual ZomeFunctionCallBackEventArgs Save(Dictionary customDataKeyValuePair = null, Dictionary holochainFieldsIsEnabledKeyValuePair = null) + public virtual ZomeFunctionCallBackEventArgs Save(Dictionary customDataKeyValuePair = null, Dictionary holochainFieldsIsEnabledKeyValuePair = null, bool cachePropertyInfos = true) { - - - - return SaveAsync(customDataKeyValuePair, holochainFieldsIsEnabledKeyValuePair).Result; + return SaveAsync(customDataKeyValuePair, holochainFieldsIsEnabledKeyValuePair, cachePropertyInfos).Result; } /// diff --git a/NextGenSoftware.Holochain.HoloNET.Client.TestHarness/Examples/HoloNETAuditEntryBaseClass Examples/Single Class Example/Avatar.cs b/NextGenSoftware.Holochain.HoloNET.Client.TestHarness/Examples/HoloNETAuditEntryBaseClass Examples/Single Class Example/Avatar.cs index 96f9fa7..6bd7751 100644 --- a/NextGenSoftware.Holochain.HoloNET.Client.TestHarness/Examples/HoloNETAuditEntryBaseClass Examples/Single Class Example/Avatar.cs +++ b/NextGenSoftware.Holochain.HoloNET.Client.TestHarness/Examples/HoloNETAuditEntryBaseClass Examples/Single Class Example/Avatar.cs @@ -29,26 +29,26 @@ public class Avatar : HoloNETAuditEntryBaseClass [HolochainFieldName("dob")] public DateTime DOB { get; set; } - public override Task SaveAsync(Dictionary customDataKeyValuePair = null, Dictionary holochainFieldsIsEnabledKeyValuePair = null) + public override Task SaveAsync(Dictionary customDataKeyValuePair = null, Dictionary holochainFieldsIsEnabledKeyValuePair = null, bool cachePropertyInfos = true) { - //Example of how to disable various holochain fields/properties so the data is omitted from the data sent to the zome function. - //if (holochainFieldsIsEnabledKeyValuePair == null) - // holochainFieldsIsEnabledKeyValuePair = new Dictionary(); + //Example of how to disable various holochain fields/ properties so the data is omitted from the data sent to the zome function. + if (holochainFieldsIsEnabledKeyValuePair == null) + holochainFieldsIsEnabledKeyValuePair = new Dictionary(); - //holochainFieldsIsEnabledKeyValuePair["DOB"] = false; - //holochainFieldsIsEnabledKeyValuePair["Email"] = false; + holochainFieldsIsEnabledKeyValuePair["DOB"] = false; + holochainFieldsIsEnabledKeyValuePair["Email"] = false; //Below is an example of how you can send custom data to the zome function: - //if (customDataKeyValuePair == null) - // customDataKeyValuePair = new Dictionary(); + if (customDataKeyValuePair == null) + customDataKeyValuePair = new Dictionary(); - //customDataKeyValuePair["dynamic data"] = "dynamic"; - //customDataKeyValuePair["some other data"] = "data"; + customDataKeyValuePair["dynamic data"] = "dynamic"; + customDataKeyValuePair["some other data"] = "data"; - return base.SaveAsync(customDataKeyValuePair, holochainFieldsIsEnabledKeyValuePair); + return base.SaveAsync(customDataKeyValuePair, holochainFieldsIsEnabledKeyValuePair, cachePropertyInfos); } - public override ZomeFunctionCallBackEventArgs Save(Dictionary customDataKeyValuePair = null, Dictionary holochainFieldsIsEnabledKeyValuePair = null) + public override ZomeFunctionCallBackEventArgs Save(Dictionary customDataKeyValuePair = null, Dictionary holochainFieldsIsEnabledKeyValuePair = null, bool cachePropertyInfos = true) { //Example of how to disable various holochain fields/properties so the data is omitted from the data sent to the zome function. //if (holochainFieldsIsEnabledKeyValuePair == null) @@ -64,7 +64,7 @@ public override ZomeFunctionCallBackEventArgs Save(Dictionary cu //customDataKeyValuePair["dynamic data"] = "dynamic"; //customDataKeyValuePair["some other data"] = "data"; - return base.Save(customDataKeyValuePair, holochainFieldsIsEnabledKeyValuePair); + return base.Save(customDataKeyValuePair, holochainFieldsIsEnabledKeyValuePair, cachePropertyInfos); } } } \ No newline at end of file diff --git a/README.md b/README.md index 91ab323..4844c09 100644 --- a/README.md +++ b/README.md @@ -1728,13 +1728,13 @@ public virtual ZomeFunctionCallBackEventArgs Load() This method will save the Holochain entry to the Holochain Conductor. This calls the [CallZomeFunction](#CallZomeFunction) on the HoloNET client passing in the zome function name specified in the constructor param `zomeCreateEntryFunction` or property [ZomeCreateEntryFunction](#ZomeCreateEntryFunction) if it is a new entry (empty object) or the `zomeUpdateEntryFunction` param and [ZomeUpdateEntryFunction](#ZomeUpdateEntryFunction) property if it's an existing entry (previously saved object containing a valid value for the [EntryHash](#EntryHash) property). Once it has saved the entry it will then update the [EntryHash](#entryHash) property with the entry hash returned from the zome call/conductor. The [PreviousVersionEntryHash](#PreviousVersionEntryHash) property is also set to the previous EntryHash (if there is one). Once it has finished saving and got a response from the Holochain Conductor it will raise the [OnSaved](#OnSaved) event. -**NOTE:** The parmeterless overload will automatically extrct the properties that need saving (contain the HolochainFieldName attribute). This method uses reflection so has a tiny performance overhead (negligbale), but if you need the extra nanoseconds use the other Save overload passing in your own params object. +**NOTE:** The overloads that do not have the paramsObject param will automatically extrct the properties that need saving (contain the HolochainFieldName attribute). This method uses reflection so has a tiny performance overhead (negligbale), but if you need the extra nanoseconds use the other Save overload passing in your own params object. **NOTE:** The corresponding rust Holochain Entry in your hApp will need to have the same properties contained in your class and have the correct mappings using the [HolochainFieldName](#HolochainFieldName) attribute. Please see [HoloNETEntryBaseClass](#HoloNETEntryBaseClass) for more info... ````c# -public virtual async Task SaveAsync(Dictionary customDataKeyValuePair = null, Dictionary holochainFieldsIsEnabledKeyValuePair = null) -public virtual ZomeFunctionCallBackEventArgs Save(Dictionary customDataKeyValuePair = null, Dictionary holochainFieldsIsEnabledKeyValuePair = null) +public virtual async Task SaveAsync(Dictionary customDataKeyValuePair = null, Dictionary holochainFieldsIsEnabledKeyValuePair = null, bool cachePropertyInfos = true) +public virtual ZomeFunctionCallBackEventArgs Save(Dictionary customDataKeyValuePair = null, Dictionary holochainFieldsIsEnabledKeyValuePair = null, bool cachePropertyInfos = true) public virtual async Task SaveAsync(dynamic paramsObject) public virtual ZomeFunctionCallBackEventArgs Save(dynamic paramsObject) ```` @@ -1744,28 +1744,29 @@ public virtual ZomeFunctionCallBackEventArgs Save(dynamic paramsObject) | paramsObject | The dynamic data object containing the params you wish to pass to the Create/Update zome function via the [CallZomeFunction](#CallZomeFunction) method. **NOTE:** You do not need to pass this in unless you have a need, if you call one of the overloads that do not have this parameter [HoloNETEntryBaseClass](#HoloNETEntryBaseClass) will automatically generate this object from any properties in your class that contain the [HolochainFieldName](#HolochainFieldName) attribute. | | customDataKeyValuePair | This is a optional dictionary containing keyvalue pairs of custom data you wish to inject into the params that are sent to the zome function. | | holochainFieldsIsEnabledKeyValuePair | This is a optional dictionary containing keyvalue pairs to allow properties that contain the HolochainFieldName to be omitted from the data sent to the zome function. The key (case senstive) needs to match a property that has the HolochainFieldName attribute. | +| cachePropertyInfos | Set this to true (default) if you want HoloNET to cache the property info's for the Entry Data Object (this can reduce the slight overhead used by reflection). | Below is an example of how to override the SaveAsync in a class that extends the [HoloNETEntryBaseClass](#HoloNETEntryBaseClass) or [HoloNETAuditEntryBaseClass](#HoloNETAuditEntryBaseClass): ````c# -public override Task SaveAsync(Dictionary customDataKeyValuePair = null, Dictionary holochainFieldsIsEnabledKeyValuePair = null) - { - //Example of how to disable various holochain fields/properties so the data is omitted from the data sent to the zome function. - if (holochainFieldsIsEnabledKeyValuePair == null) - holochainFieldsIsEnabledKeyValuePair = new Dictionary(); +public override Task SaveAsync(Dictionary customDataKeyValuePair = null, Dictionary holochainFieldsIsEnabledKeyValuePair = null, bool cachePropertyInfos = true) +{ + //Example of how to disable various holochain fields/ properties so the data is omitted from the data sent to the zome function. + if (holochainFieldsIsEnabledKeyValuePair == null) + holochainFieldsIsEnabledKeyValuePair = new Dictionary(); - holochainFieldsIsEnabledKeyValuePair["DOB"] = false; - holochainFieldsIsEnabledKeyValuePair["Email"] = false; + holochainFieldsIsEnabledKeyValuePair["DOB"] = false; + holochainFieldsIsEnabledKeyValuePair["Email"] = false; - //Below is an example of how you can send custom data to the zome function: - if (customDataKeyValuePair == null) - customDataKeyValuePair = new Dictionary(); + //Below is an example of how you can send custom data to the zome function: + if (customDataKeyValuePair == null) + customDataKeyValuePair = new Dictionary(); - customDataKeyValuePair["dynamic data"] = "dynamic"; - customDataKeyValuePair["some other data"] = "data"; + customDataKeyValuePair["dynamic data"] = "dynamic"; + customDataKeyValuePair["some other data"] = "data"; - return base.SaveAsync(customDataKeyValuePair, holochainFieldsIsEnabledKeyValuePair); - } + return base.SaveAsync(customDataKeyValuePair, holochainFieldsIsEnabledKeyValuePair, cachePropertyInfos); +} ```` This example is taken from the Avatar class in the Single Class Example folder in the HoloNET Test Harness.