From 64462996cae71a2aadabdff7b7cb46f3970b8f4c Mon Sep 17 00:00:00 2001 From: james kim Date: Fri, 24 Aug 2018 11:20:32 +1000 Subject: [PATCH] Added caching option --- .../PerplexUmbracoForms.config | 4 + Perplex.Umbraco.Forms/Code/ArrayExtensions.cs | 48 ++++++++ Perplex.Umbraco.Forms/Code/CacheService.cs | 71 ++++++++++++ .../Code/Configuration/ExtensionConfig.cs | 7 +- .../Code/Configuration/FieldTypeConfig.cs | 4 - .../Configuration/PerplexBaseFileConfig.cs | 6 +- .../Code/Configuration/PerplexCacheConfig.cs | 14 +++ .../Configuration/PerplexFileUploadConfig.cs | 7 +- .../Configuration/PerplexImageUploadConfig.cs | 7 +- .../Configuration/PerplexRecaptchaConfig.cs | 7 +- .../PerplexUmbracoFormsConfig.cs | 12 +- Perplex.Umbraco.Forms/Code/ICacheService.cs | 15 +++ .../Code/ObjectExtensions.cs | 104 ++++++++++++++++++ Perplex.Umbraco.Forms/Code/UmbracoEvents.cs | 29 ++++- .../Controllers/PerplexFormTreeController.cs | 60 ++++++---- .../PerplexUmbracoFormController.cs | 18 +-- .../Perplex.Umbraco.Forms.csproj | 5 + Perplex.Umbraco.Forms/packages.config | 1 + 18 files changed, 345 insertions(+), 74 deletions(-) create mode 100644 Perplex.Umbraco.Forms/Code/ArrayExtensions.cs create mode 100644 Perplex.Umbraco.Forms/Code/CacheService.cs create mode 100644 Perplex.Umbraco.Forms/Code/Configuration/PerplexCacheConfig.cs create mode 100644 Perplex.Umbraco.Forms/Code/ICacheService.cs create mode 100644 Perplex.Umbraco.Forms/Code/ObjectExtensions.cs diff --git a/Perplex.Umbraco.Forms/App_Plugins/PerplexUmbracoForms/PerplexUmbracoForms.config b/Perplex.Umbraco.Forms/App_Plugins/PerplexUmbracoForms/PerplexUmbracoForms.config index 0e560b1..7b24969 100644 --- a/Perplex.Umbraco.Forms/App_Plugins/PerplexUmbracoForms/PerplexUmbracoForms.config +++ b/Perplex.Umbraco.Forms/App_Plugins/PerplexUmbracoForms/PerplexUmbracoForms.config @@ -44,4 +44,8 @@ + + true + 10 + \ No newline at end of file diff --git a/Perplex.Umbraco.Forms/Code/ArrayExtensions.cs b/Perplex.Umbraco.Forms/Code/ArrayExtensions.cs new file mode 100644 index 0000000..90df961 --- /dev/null +++ b/Perplex.Umbraco.Forms/Code/ArrayExtensions.cs @@ -0,0 +1,48 @@ +using System; + +namespace PerplexUmbraco.Forms.Code +{ + public static class ArrayExtensions + { + public static void ForEach(this Array array, Action action) + { + if (array.LongLength == 0) return; + ArrayTraverse walker = new ArrayTraverse(array); + do action(array, walker.Position); + while (walker.Step()); + } + } + + internal class ArrayTraverse + { + public int[] Position; + private int[] maxLengths; + + public ArrayTraverse(Array array) + { + maxLengths = new int[array.Rank]; + for (int i = 0; i < array.Rank; ++i) + { + maxLengths[i] = array.GetLength(i) - 1; + } + Position = new int[array.Rank]; + } + + public bool Step() + { + for (int i = 0; i < Position.Length; ++i) + { + if (Position[i] < maxLengths[i]) + { + Position[i]++; + for (int j = 0; j < i; j++) + { + Position[j] = 0; + } + return true; + } + } + return false; + } + } +} diff --git a/Perplex.Umbraco.Forms/Code/CacheService.cs b/Perplex.Umbraco.Forms/Code/CacheService.cs new file mode 100644 index 0000000..4a34072 --- /dev/null +++ b/Perplex.Umbraco.Forms/Code/CacheService.cs @@ -0,0 +1,71 @@ +using System; + +using Umbraco.Core; +using Umbraco.Core.Logging; + +namespace PerplexUmbraco.Forms.Code +{ + public class CacheService : ICacheService + { + public void SetRequestCache(string cacheKey, object cacheObject) + { + if (cacheObject != null) + { + ApplicationContext.Current.ApplicationCache.RequestCache.GetCacheItem(cacheKey, () => cacheObject); + return; + } + + LogHelper.Warn($"Failed to cache null object with key: {cacheKey}"); + } + + public T GetRequestCache(string cacheKey) + { + var cachedObject = ApplicationContext.Current.ApplicationCache.RequestCache.GetCacheItem(cacheKey); + + if (cachedObject == null) + { + return default(T); + } + + return (T)cachedObject; + } + + public void SetRuntimeCache(string cacheKey, object cacheObject, TimeSpan duration) + { + SetRuntimeCache(cacheKey, cacheObject, duration, false); + } + + public void SetRuntimeCache(string cacheKey, object cacheObject, TimeSpan duration, bool isSliding) + { + if (cacheObject != null) + { + ApplicationContext.Current.ApplicationCache.RuntimeCache.InsertCacheItem(cacheKey, cacheObject.Copy, duration, isSliding); + return; + } + + LogHelper.Warn($"Failed to cache null object with key: {cacheKey}"); + } + + public void ClearRuntimeCacheByKey(string cacheKey) + { + ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheItem(cacheKey); + } + + public void ClearRuntimeCacheByPrefix(string prefix) + { + ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(prefix); + } + + public T GetRuntimeCache(string cacheKey) + { + var cachedObject = ApplicationContext.Current.ApplicationCache.RuntimeCache.GetCacheItem(cacheKey); + + if (cachedObject == null) + { + return default(T); + } + + return ((T)cachedObject).Copy(); + } + } +} diff --git a/Perplex.Umbraco.Forms/Code/Configuration/ExtensionConfig.cs b/Perplex.Umbraco.Forms/Code/Configuration/ExtensionConfig.cs index 56f1479..e6f0014 100644 --- a/Perplex.Umbraco.Forms/Code/Configuration/ExtensionConfig.cs +++ b/Perplex.Umbraco.Forms/Code/Configuration/ExtensionConfig.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Xml.Serialization; +using System.Xml.Serialization; namespace PerplexUmbraco.Forms.Code.Configuration { diff --git a/Perplex.Umbraco.Forms/Code/Configuration/FieldTypeConfig.cs b/Perplex.Umbraco.Forms/Code/Configuration/FieldTypeConfig.cs index bf64c14..aff0b3a 100644 --- a/Perplex.Umbraco.Forms/Code/Configuration/FieldTypeConfig.cs +++ b/Perplex.Umbraco.Forms/Code/Configuration/FieldTypeConfig.cs @@ -1,8 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using System.Xml.Serialization; namespace PerplexUmbraco.Forms.Code.Configuration diff --git a/Perplex.Umbraco.Forms/Code/Configuration/PerplexBaseFileConfig.cs b/Perplex.Umbraco.Forms/Code/Configuration/PerplexBaseFileConfig.cs index 8aa2867..4694db1 100644 --- a/Perplex.Umbraco.Forms/Code/Configuration/PerplexBaseFileConfig.cs +++ b/Perplex.Umbraco.Forms/Code/Configuration/PerplexBaseFileConfig.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Collections.Generic; using System.Xml.Serialization; namespace PerplexUmbraco.Forms.Code.Configuration diff --git a/Perplex.Umbraco.Forms/Code/Configuration/PerplexCacheConfig.cs b/Perplex.Umbraco.Forms/Code/Configuration/PerplexCacheConfig.cs new file mode 100644 index 0000000..0d2a328 --- /dev/null +++ b/Perplex.Umbraco.Forms/Code/Configuration/PerplexCacheConfig.cs @@ -0,0 +1,14 @@ +using System.Xml.Serialization; + +namespace PerplexUmbraco.Forms.Code.Configuration +{ + [XmlType("PerplexCacheConfig")] + public class PerplexCacheConfig + { + [XmlElement("CacheDurationInMinutes")] + public int CacheDurationInMinutes { get; set; } + + [XmlElement("EnableCache")] + public bool EnableCache { get; set; } + } +} diff --git a/Perplex.Umbraco.Forms/Code/Configuration/PerplexFileUploadConfig.cs b/Perplex.Umbraco.Forms/Code/Configuration/PerplexFileUploadConfig.cs index 9f98e85..cd88ec5 100644 --- a/Perplex.Umbraco.Forms/Code/Configuration/PerplexFileUploadConfig.cs +++ b/Perplex.Umbraco.Forms/Code/Configuration/PerplexFileUploadConfig.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Xml.Serialization; +using System.Xml.Serialization; namespace PerplexUmbraco.Forms.Code.Configuration { diff --git a/Perplex.Umbraco.Forms/Code/Configuration/PerplexImageUploadConfig.cs b/Perplex.Umbraco.Forms/Code/Configuration/PerplexImageUploadConfig.cs index 5e9ad34..ebf9f3f 100644 --- a/Perplex.Umbraco.Forms/Code/Configuration/PerplexImageUploadConfig.cs +++ b/Perplex.Umbraco.Forms/Code/Configuration/PerplexImageUploadConfig.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Xml.Serialization; +using System.Xml.Serialization; namespace PerplexUmbraco.Forms.Code.Configuration { diff --git a/Perplex.Umbraco.Forms/Code/Configuration/PerplexRecaptchaConfig.cs b/Perplex.Umbraco.Forms/Code/Configuration/PerplexRecaptchaConfig.cs index 54477d9..cf76d3c 100644 --- a/Perplex.Umbraco.Forms/Code/Configuration/PerplexRecaptchaConfig.cs +++ b/Perplex.Umbraco.Forms/Code/Configuration/PerplexRecaptchaConfig.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Xml.Serialization; +using System.Xml.Serialization; namespace PerplexUmbraco.Forms.Code.Configuration { diff --git a/Perplex.Umbraco.Forms/Code/Configuration/PerplexUmbracoFormsConfig.cs b/Perplex.Umbraco.Forms/Code/Configuration/PerplexUmbracoFormsConfig.cs index 6c876da..27fc3ce 100644 --- a/Perplex.Umbraco.Forms/Code/Configuration/PerplexUmbracoFormsConfig.cs +++ b/Perplex.Umbraco.Forms/Code/Configuration/PerplexUmbracoFormsConfig.cs @@ -1,11 +1,9 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using System.Web.Hosting; using System.Xml.Serialization; + using static PerplexUmbraco.Forms.Code.Constants; namespace PerplexUmbraco.Forms.Code.Configuration @@ -73,6 +71,8 @@ public static void CreateIfNotExists() public PerplexRecaptchaConfig PerplexRecaptchaConfig { get; set; } + public PerplexCacheConfig PerplexCacheConfig { get; set; } + private static string GetFilePath() { return HostingEnvironment.MapPath(Constants.CONFIGURATION_FILE_PATH); @@ -139,6 +139,12 @@ private static string GetFilePath() PerplexRecaptchaConfig = new PerplexRecaptchaConfig { ErrorMessage = "" + }, + + PerplexCacheConfig = new PerplexCacheConfig + { + EnableCache = false, + CacheDurationInMinutes = 10 } }; } diff --git a/Perplex.Umbraco.Forms/Code/ICacheService.cs b/Perplex.Umbraco.Forms/Code/ICacheService.cs new file mode 100644 index 0000000..c26cae1 --- /dev/null +++ b/Perplex.Umbraco.Forms/Code/ICacheService.cs @@ -0,0 +1,15 @@ +using System; + +namespace PerplexUmbraco.Forms.Code +{ + public interface ICacheService + { + T GetRequestCache(string cacheKey); + void SetRequestCache(string cacheKey, object cacheObject); + T GetRuntimeCache(string cacheKey); + void SetRuntimeCache(string cacheKey, object cacheObject, TimeSpan duration); + void SetRuntimeCache(string cacheKey, object cacheObject, TimeSpan duration, bool isSliding); + void ClearRuntimeCacheByKey(string cacheKey); + void ClearRuntimeCacheByPrefix(string prefix); + } +} diff --git a/Perplex.Umbraco.Forms/Code/ObjectExtensions.cs b/Perplex.Umbraco.Forms/Code/ObjectExtensions.cs new file mode 100644 index 0000000..b9cc988 --- /dev/null +++ b/Perplex.Umbraco.Forms/Code/ObjectExtensions.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Reflection; + +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace PerplexUmbraco.Forms.Code +{ + public static class ObjectExtensions + { + private static readonly MethodInfo CloneMethod = typeof(Object).GetMethod("MemberwiseClone", BindingFlags.NonPublic | BindingFlags.Instance); + + public static bool IsPrimitive(this Type type) + { + if (type == typeof(String)) return true; + return (type.IsValueType & type.IsPrimitive); + } + + public static Object Copy(this Object originalObject) + { + return InternalCopy(originalObject, new Dictionary(new ReferenceEqualityComparer())); + } + + public static T Copy(this T original) + { + return (T)Copy((Object)original); + } + + public static bool IsNullOrWhiteSpaceString(this object item) + { + return string.IsNullOrWhiteSpace(item?.ToString()); + } + + public static string ToJson(this object o, bool camelCase = false) + { + var settings = new JsonSerializerSettings(); + if (camelCase) + { + settings.ContractResolver = new CamelCasePropertyNamesContractResolver(); + } + + return JsonConvert.SerializeObject(o, settings); + } + + private static Object InternalCopy(Object originalObject, IDictionary visited) + { + if (originalObject == null) return null; + var typeToReflect = originalObject.GetType(); + if (IsPrimitive(typeToReflect)) return originalObject; + if (visited.ContainsKey(originalObject)) return visited[originalObject]; + if (typeof(Delegate).IsAssignableFrom(typeToReflect)) return null; + var cloneObject = CloneMethod.Invoke(originalObject, null); + if (typeToReflect.IsArray) + { + var arrayType = typeToReflect.GetElementType(); + if (IsPrimitive(arrayType) == false) + { + Array clonedArray = (Array)cloneObject; + clonedArray.ForEach((array, indices) => array.SetValue(InternalCopy(clonedArray.GetValue(indices), visited), indices)); + } + + } + visited.Add(originalObject, cloneObject); + CopyFields(originalObject, visited, cloneObject, typeToReflect); + RecursiveCopyBaseTypePrivateFields(originalObject, visited, cloneObject, typeToReflect); + return cloneObject; + } + + private static void RecursiveCopyBaseTypePrivateFields(object originalObject, IDictionary visited, object cloneObject, Type typeToReflect) + { + if (typeToReflect.BaseType != null) + { + RecursiveCopyBaseTypePrivateFields(originalObject, visited, cloneObject, typeToReflect.BaseType); + CopyFields(originalObject, visited, cloneObject, typeToReflect.BaseType, BindingFlags.Instance | BindingFlags.NonPublic, info => info.IsPrivate); + } + } + + private static void CopyFields(object originalObject, IDictionary visited, object cloneObject, Type typeToReflect, BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy, Func filter = null) + { + foreach (FieldInfo fieldInfo in typeToReflect.GetFields(bindingFlags)) + { + if (filter != null && filter(fieldInfo) == false) continue; + if (IsPrimitive(fieldInfo.FieldType)) continue; + var originalFieldValue = fieldInfo.GetValue(originalObject); + var clonedFieldValue = InternalCopy(originalFieldValue, visited); + fieldInfo.SetValue(cloneObject, clonedFieldValue); + } + } + } + + public class ReferenceEqualityComparer : EqualityComparer + { + public override bool Equals(object x, object y) + { + return ReferenceEquals(x, y); + } + public override int GetHashCode(object obj) + { + if (obj == null) return 0; + return obj.GetHashCode(); + } + } +} diff --git a/Perplex.Umbraco.Forms/Code/UmbracoEvents.cs b/Perplex.Umbraco.Forms/Code/UmbracoEvents.cs index 69214d2..f773076 100644 --- a/Perplex.Umbraco.Forms/Code/UmbracoEvents.cs +++ b/Perplex.Umbraco.Forms/Code/UmbracoEvents.cs @@ -1,12 +1,14 @@ -using System.Linq; +using System; +using System.Linq; using System.Web; + +using PerplexUmbraco.Forms.Code.Configuration; + using Umbraco.Core; using Umbraco.Core.Logging; -using PerplexUmbraco.Forms.Code.Configuration; -using Umbraco.Web; -using Umbraco.Forms.Data.Storage; -using System; using Umbraco.Forms.Core; +using Umbraco.Forms.Data.Storage; +using Umbraco.Web; namespace PerplexUmbraco.Forms.Code { @@ -64,6 +66,8 @@ void FormStorage_Created(object sender, FormEventArgs e) folder.Forms.Add(form.Id.ToString()); PerplexFolder.SaveAll(); + + ClearFormsCache(folderId.ToString()); } void FormStorage_Deleted(object sender, FormEventArgs e) @@ -75,7 +79,20 @@ void FormStorage_Deleted(object sender, FormEventArgs e) { folder.Forms.Remove(form.Id.ToString()); PerplexFolder.SaveAll(); + + ClearFormsCache(folder.Id); + } + } + + void ClearFormsCache(string folderId) + { + var cacheConfig = PerplexUmbracoFormsConfig.Get.PerplexCacheConfig; + + if (cacheConfig.EnableCache) + { + var cacheService = new CacheService(); + cacheService.ClearRuntimeCacheByKey($"PerplexFormTreeController_GetTreeNodes_id:{folderId}"); } } } -} \ No newline at end of file +} diff --git a/Perplex.Umbraco.Forms/Controllers/PerplexFormTreeController.cs b/Perplex.Umbraco.Forms/Controllers/PerplexFormTreeController.cs index 7ee2956..f8ad9cc 100644 --- a/Perplex.Umbraco.Forms/Controllers/PerplexFormTreeController.cs +++ b/Perplex.Umbraco.Forms/Controllers/PerplexFormTreeController.cs @@ -1,23 +1,18 @@ -using Newtonsoft.Json; -using PerplexUmbraco.Forms.Code; -using System; -using System.Collections.Generic; -using System.IO; +using System; using System.Linq; using System.Net.Http.Formatting; -using System.Text; -using System.Threading.Tasks; using System.Web; -using System.Web.Hosting; -using System.Web.Http; -using Umbraco.Forms.Data; + +using PerplexUmbraco.Forms.Code; +using PerplexUmbraco.Forms.Code.Configuration; + +using umbraco; +using umbraco.BusinessLogic.Actions; using Umbraco.Forms.Web.Trees; +using Umbraco.Web; using Umbraco.Web.Models.Trees; using Umbraco.Web.Mvc; using Umbraco.Web.Trees; -using Umbraco.Web; -using umbraco.BusinessLogic.Actions; -using umbraco; namespace PerplexUmbraco.Forms.Controllers { @@ -41,22 +36,49 @@ public class PerplexFormTreeController : FormTreeController { // We load our custom menu actions from our own folder private const string VIEWS_ROOT = "/App_Plugins/PerplexUmbracoForms/views/"; + private readonly ICacheService _cacheService; + private readonly PerplexCacheConfig _cacheConfig; - public PerplexFormTreeController() { } + public PerplexFormTreeController() + { + _cacheService = new CacheService(); + _cacheConfig = PerplexUmbracoFormsConfig.Get.PerplexCacheConfig; + } - protected override Umbraco.Web.Models.Trees.TreeNodeCollection GetTreeNodes(string id, System.Net.Http.Formatting.FormDataCollection queryStrings) + protected override TreeNodeCollection GetTreeNodes(string id, FormDataCollection queryStrings) { + var cacheKey = $"PerplexFormTreeController_GetTreeNodes_id:{id}"; + // If this is a form, use Umbraco's default behavior var folder = PerplexFolder.Get(id); if (folder == null) { - return base.GetTreeNodes(id, queryStrings); + var treeNodeCollection = _cacheConfig.EnableCache ? _cacheService.GetRuntimeCache(cacheKey) : (TreeNodeCollection)null; + + if (treeNodeCollection == null) + { + treeNodeCollection = base.GetTreeNodes(id, queryStrings); + + if (_cacheConfig.EnableCache) + _cacheService.SetRuntimeCache(cacheKey, treeNodeCollection, new TimeSpan(0, _cacheConfig.CacheDurationInMinutes, 0), true); + } + + return treeNodeCollection; } // This is a folder - // We require all forms, and apply filtering based on folders later - var baseTreeNodes = base.GetTreeNodes("-1", queryStrings); + cacheKey = $"PerplexFormTreeController_GetTreeNodes_id:-1"; + var baseTreeNodes = _cacheConfig.EnableCache ? _cacheService.GetRuntimeCache(cacheKey) : (TreeNodeCollection)null; + + if (baseTreeNodes == null) + { + // We require all forms, and apply filtering based on folders later + baseTreeNodes = base.GetTreeNodes("-1", queryStrings); + + if (_cacheConfig.EnableCache) + _cacheService.SetRuntimeCache(cacheKey, baseTreeNodes, new TimeSpan(0, _cacheConfig.CacheDurationInMinutes, 0), true); + } // Sanity check; make sure there are no orphan forms around // (forms not contained within any folder). If so, move them to the root folder @@ -92,7 +114,7 @@ protected override Umbraco.Web.Models.Trees.TreeNodeCollection GetTreeNodes(stri // Add any subfolders of this node // We loop through the list in reverse as we add every folder at the start of the list (before forms) - foreach (var subFolder in folder.Folders.Reverse()) + foreach (var subFolder in folder.Folders.Reverse()) { // If this subfolder is disabled, and it is not on a path towards // a folder that is NOT disabled, it should not be listed at all. diff --git a/Perplex.Umbraco.Forms/Controllers/PerplexUmbracoFormController.cs b/Perplex.Umbraco.Forms/Controllers/PerplexUmbracoFormController.cs index 19aa2b9..198af4f 100644 --- a/Perplex.Umbraco.Forms/Controllers/PerplexUmbracoFormController.cs +++ b/Perplex.Umbraco.Forms/Controllers/PerplexUmbracoFormController.cs @@ -1,23 +1,15 @@ -using Newtonsoft.Json; -using PerplexUmbraco.Forms.Code; -using PerplexUmbraco.Forms.Code.Configuration; -using System; +using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Web; -using System.Web.Hosting; using System.Web.Http; -using Umbraco.Core.IO; -using Umbraco.Forms.Core.Providers; -using Umbraco.Forms.Data; + +using PerplexUmbraco.Forms.Code; +using PerplexUmbraco.Forms.Code.Configuration; + using Umbraco.Forms.Data.Storage; -using Umbraco.Forms.Mvc.Models.Backoffice; -using Umbraco.Forms.Web.Models.Backoffice; -using Umbraco.Web.Editors; -using Umbraco.Web.Mvc; using Umbraco.Web.WebApi; namespace PerplexUmbraco.Forms.Controllers diff --git a/Perplex.Umbraco.Forms/Perplex.Umbraco.Forms.csproj b/Perplex.Umbraco.Forms/Perplex.Umbraco.Forms.csproj index 23d0741..bf04c5d 100644 --- a/Perplex.Umbraco.Forms/Perplex.Umbraco.Forms.csproj +++ b/Perplex.Umbraco.Forms/Perplex.Umbraco.Forms.csproj @@ -230,6 +230,10 @@ + + + + @@ -239,6 +243,7 @@ + diff --git a/Perplex.Umbraco.Forms/packages.config b/Perplex.Umbraco.Forms/packages.config index 46aa4a3..e94952e 100644 --- a/Perplex.Umbraco.Forms/packages.config +++ b/Perplex.Umbraco.Forms/packages.config @@ -36,6 +36,7 @@ +