AppFabric Caching in Orchard CMS

#appfabric

Posted by admin on September 01, 2019

A while ago I was commissioned to write an AppFabric Caching module for Onestop Internet Ltd, who have subsequently generously donated the module to the wider community. Well, what is AppFabric Caching? And how does it help me? AppFabric Caching is a distributed caching mechanism that allows multiple web applications to use the same cache, this is different to how most websites currently work, even Orchard by default maintains its cache within its separate website instances. With AppFabric, all website instances maintain and contribute to the same cache. If you are at all interested in the Concepts and Architecture of AppFabric please visit Concepts and Architecture (Windows Server AppFabric Caching)

Implementation

To implement AppFabric caching please follow these steps.

  1. Install AppFabric Cache Server V1.1 (Microsoft AppFabric 1.1 for Windows Server) It is VERY important that it is this version and NOT V1.0

  2. Once installed you will have a PowerShell command in your start menu… image Open this up and run the following command Update-CacheHostAllowedVersions 3 3 3 3 This will update you cache server to the right version of the DLLs you are going to use, I would then restart your cache cluster (Restart-CacheCluster)

    image

  3. Download the AppFabric Module instance to your modules directory. Currently here (https://orchardappfabric.codeplex.com)

  4. Open up you Web.Config file in Orchard.Web and add the following configuration… Add this to your <ConfigSections>

    Next, add this anywhere in your config file.

    <datacacheclients><datacacheclient name="default"></datacacheclient></datacacheclients>
          <securityProperties mode="None" protectionLevel="None">
            <!--<messageSecurity
              authorizationInfo="Your authorization token will be here.">
            </messageSecurity>-->
          </securityProperties>
        </dataCacheClient>
A few things to note at this point.

*   Your **dataCacheClient name** is the **name of your Tenant (default should ALWAYS be left lowercase)**

*   Your **hostname** and **cachePort** are the same as the one your screenshot above, if you have forgotten, jump on to PowerShell and type Get-Cache

*   The SecurityProperties attribute MUST be in there. I have set it to `none` in PowerShell, but this is down to the individual configuration you have set. – **If this does not match, AppFabric will fall over in a mess.**

</section>
  1. Enable AppFabric module and you are good to go.

Some important URLs

Once you have everything working, you can view some stuff in Orchard…

View your Cache: http://localhost:30320/OrchardLocal/Appfabric/Index

Clear Cache for Tenant: http://localhost:30320/OrchardLocal/Appfabric/Clear

Constraining the AppFabric Module

Out of the box, the AppFabric Module attempts to cache everything within distributed cache apart from ShapeDescriptor, ShapeTable and ContentTypeDefinition.

If you want your object cached only locally and not in AppFabric, then create an implementation of ICacheStoreStrategy.

image

Returning true/false will force local cache to take over on that particular object.

Challenges faced?

Serialization issues:

By default AppFabric Serializes objects using the DataContactSerializer, which means that you need a couple of things:

  • Default Constructor
  • Mark entities' with the [Serialization] attribute

This posed a major problem as this was a MASSIVE constraint, the whole point of Orchard is to allow people to create objects and cache them and not have to worry about external stuff like “how does it cache?” but just know that somehow, it does. To solve this issue I created a wrapper object and marked that as Serializable: (code does need to be cleaned up)

    [Serializable]
    public class EntryWrapper<T> : ISerializable {
        private readonly Lazy<T> _value;

        [XmlIgnore]
        public T Value {
            get { return _value.Value; }
        }

        public EntryWrapper(T value) {
            _value = new Lazy<T>(() => value);
        }

        protected EntryWrapper(SerializationInfo info, StreamingContext context) {
            var settings = new JsonSerializerSettings {
                Converters = new List<JsonConverter> {new MultipleConstructorsJsonConverter(), new InterfaceJsonConverter()},
                TypeNameHandling = TypeNameHandling.All,
                ContractResolver = new DefaultContractResolver {
                    DefaultMembersSearchFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance
                },
                ReferenceLoopHandling = ReferenceLoopHandling.Serialize,
                ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor
            };

            _value = new> Lazy<T>(() => JsonConvert.DeserializeObject<T>(info.GetString("JsonValue"), settings));
        }

        [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)]
        void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context) {
            var settings = new JsonSerializerSettings {
                TypeNameHandling = TypeNameHandling.All,
                ContractResolver = new DefaultContractResolver {
                    DefaultMembersSearchFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance
                },
                ReferenceLoopHandling = ReferenceLoopHandling.Serialize,
                ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor
            };
            // Do JSON serialization of the underlying object here
            var json = JsonConvert.SerializeObject(Value, settings);
            info.AddValue("JsonValue", json);
        }
    }

I then implemented Json.Net to Serialize/De-serialize said object, this works perfectly fine in the majority of all cases, except when that object as multiple constructors, or was an interface, for this I created some JsonConverters. I am not posting the code here, but you can view them over on Codeplex https://orchardappfabric.codeplex.com

Case Sensitive Cache Names

One thing I found out that wasted A LOT of time was that cache names are case sensitive, you can have two caches, one called “Default” and another called “default” (note the lower and upper case ‘Dd’). By default, AppFabric has the later out of the box, which then tries to Orchards Default tenant which cannot be renamed. I therefore imposed a constraint on the cache names ‘Default’ by always setting it to lowercase.

    var cacheName = _shellSettings.Name;
    if (cacheName.Equals("default", StringComparison.OrdinalIgnoreCase))
        cacheName = cacheName.ToLowerInvariant();

In all other cases, the cache name is the one specified within your web.config, which should also map to your Tenant.

What’s next?

Multi Datacentre Support So please give it a go, and let me know how you get on. If you have any suggestions/comments/feature etc, let me know and I will do my best to include them.