Tech Musings Just another tip's, tricks, and how-to's blog

31May/115

Shape Method Caching

So we're preparing to launch our Orchard site, and one of the final steps is tuning the performance of the site. The Orchard team realizes that caching is a huge part of a CMS, but has left the framework open in such a way that a caching module should be relatively simple to write, so we did just that.

The way we use Orchard may be a little different than most, in that our site is almost entirely composed of little widgets of different pieces of content, but we don't use widgets at all because we found they did not suit our needs properly. Instead we use [Shape] methods to define a shape name, and then a corresponding view to go behind, as so:

        [Shape]
        public IHtmlString SidebarAd(dynamic Display) {
            var ads = ContentManager.Query<HouseAdPart>(VersionOptions.Latest).List().ToList();
            return Display.SidebarAdView(Ads: ads);

        }

This allows us to do Display.SidebarAd() from anywhere else on the site to plop this particular shape in wherever, and we maintain our separation of concerns by using a shape method as a sort of controller, with a view behind it called SidebarAdView (which is a razor view).

As I started investigating how we might go about caching, I decided that we would get about 90% cache coverage on our site by just being able to add an attribute to these shape methods designating that they should be cached. (The other 10% are just razor views that don't have a shape method in front of them, but will likely be refactored to have one soon).

To start out, I basically duplicated the structure of the Orchard team's ShapeAttribute, and called it CacheAttribute:

public class CacheAttribute : Attribute
    {
        public CacheAttribute() {
            Duration = 60 * 5;
        }
        public CacheAttribute(int duration)
        {
            Duration = duration;
        }

        public int Duration
        {
            get;
            private set;
        }
    }
public class CacheAttributeOccurence {

        public CacheAttributeOccurence(CacheAttribute cacheAttribute, MethodInfo methodInfo, IComponentRegistration registration) {
            CacheAttribute = cacheAttribute;
            MethodInfo = methodInfo;
            Registration = registration;
        }

        public CacheAttribute CacheAttribute { get; private set; }
        public MethodInfo MethodInfo { get; private set; }
        public IComponentRegistration Registration { get; private set; }

    }
public class CacheAttributeBindingModule : Module {
        readonly List<CacheAttributeOccurence> _occurences = new List<CacheAttributeOccurence>();

        protected override void Load(ContainerBuilder builder) {
            builder.RegisterInstance(_occurences).As<IEnumerable<CacheAttributeOccurence>>();
        }
        protected override void AttachToComponentRegistration(IComponentRegistry componentRegistry, IComponentRegistration registration) {
            var occurences = registration.Activator.LimitType.GetMethods()
                .SelectMany(mi => mi.GetCustomAttributes(typeof (CacheAttribute), false).OfType<CacheAttribute>()
                    .Select(ca => new CacheAttributeOccurence(ca,mi,registration))
                ).ToArray();
            if (occurences.Any())
                _occurences.AddRange(occurences);
        }
    }

So now I can implement a binding strategy that enumerates each CacheAttributeOccurence and configures caching for each shape method. I started out by just configuring a standard IShapeTableProvider like so:

public class CacheAttributeBindingStrategy : IShapeTableProvider
    {
        private readonly IEnumerable<CacheAttributeOccurence> _occurrences;
        private readonly Cache<string, IHtmlString> _cache;

        private readonly IWorkContextAccessor _workContextAccessor;
        private readonly IHttpContextAccessor _httpContextAccessor;
        public CacheAttributeBindingStrategy(IEnumerable<CacheAttributeOccurence> occurences, ICacheManager cache, IWorkContextAccessor workContextAccessor, IHttpContextAccessor httpContextAccessor)
        {
            _workContextAccessor = workContextAccessor;
            _httpContextAccessor = httpContextAccessor;
            _occurrences = occurences;

            _cache = cache.GetCache<string, IHtmlString>() as Cache<string, IHtmlString>;
        }

        public void Discover(ShapeTableBuilder builder)
        {
            foreach (var occurence in _occurrences)
            {
                var shape = occurence.MethodInfo.GetCustomAttributes(typeof(ShapeAttribute), false).OfType<ShapeAttribute>().FirstOrDefault();
                if (shape != null)
                {
                    var shapeType = shape.ShapeType ?? occurence.MethodInfo.Name;
                    CacheAttributeOccurence occurence1 = occurence;
                    builder.Describe(shapeType)
                        .OnDisplaying(displaying =>
                        {

                        })
                        .OnDisplayed(displayed =>
                        {

                        });
                }
            }
        }

I had read a comment in the forums where Bertrand explained how caching could be just as simple as implementing OnDisplaying and OnDisplayed for a shape and populating the ChildContent if a shape exists in cache already.
The trouble was, how could I construct a key for my cache that can be autogenerated based on the parameter names and values for a shape method execution context. As it turns out, my answer again lay in the implementation of the ShapeAttribute. So I grabbed a few methods from their implementation, modified them a bit to fit my needs, and implemented them into my CacheAttributeBindingStrategy:

public static bool TypeImplementsInterface(Type type, Type interfaceType)
        {
            string interfaceFullName = interfaceType.FullName;
            return type.GetInterface(interfaceFullName) != null;
        }
        private static KeyValuePair<string, string> BindParameter(dynamic shape, ParameterInfo parameter)
        {
            if (parameter.Name == "Shape") return new KeyValuePair<string, string>(null, null);

            if (parameter.Name == "Display") return new KeyValuePair<string, string>(null, null);

            if (parameter.Name == "Output" && parameter.ParameterType == typeof(TextWriter))
                return new KeyValuePair<string, string>(null, null);

            // meh--
            if (parameter.Name == "Html")
            {
                return new KeyValuePair<string, string>(null, null);
            }

            var getter = _getters.GetOrAdd(parameter.Name, n =>
                CallSite<Func<CallSite, object, object>>.Create(
                Microsoft.CSharp.RuntimeBinder.Binder.GetMember(
                CSharpBinderFlags.None, n, null, new[] { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) })));

            var result = getter.Target(getter, shape);

            if (result == null)
                return new KeyValuePair<string, string>(null, null);

            switch (Type.GetTypeCode(parameter.ParameterType))
            {
                case TypeCode.Boolean:
                case TypeCode.String:
                case TypeCode.Char:
                case TypeCode.DateTime:
                case TypeCode.Int16:
                case TypeCode.Int32:
                case TypeCode.Int64:
                case TypeCode.UInt16:
                case TypeCode.UInt32:
                case TypeCode.UInt64:
                case TypeCode.Single:
                case TypeCode.Double:
                case TypeCode.Decimal:
                case TypeCode.SByte:
                case TypeCode.Byte:
                    var converter = _converters.GetOrAdd(parameter.ParameterType, CompileConverter);
                    var argument = converter.Invoke(result);
                    return new KeyValuePair<string, string>(parameter.Name, argument.ToString());
                    break;
                case TypeCode.Object:
                    if (TypeImplementsInterface(parameter.ParameterType, typeof(IContent)))
                    {
                        var converter2 = _converters.GetOrAdd(parameter.ParameterType, CompileConverter);
                        var argument2 = converter2.Invoke(result) as IContent;
                        if (argument2 != null)
                            return new KeyValuePair<string, string>(parameter.Name, argument2.Id.ToString());
                        else
                            return new KeyValuePair<string, string>(parameter.Name, "NULL");

                    }
                    return new KeyValuePair<string, string>(null, null);
                case TypeCode.DBNull:
                case TypeCode.Empty:
                default:
                    return new KeyValuePair<string, string>(null, null);
            }

            return new KeyValuePair<string, string>(null, null);

        }
        private static string ConstructCacheKey(string type, Shape shape, CacheContext cacheContext)
        {
            var arguments = cacheContext.MethodInfo.GetParameters()
                .Select(parameter => BindParameter(shape, parameter));

            var strings = new List<string>(arguments.Count() * 2);
            foreach (var p in arguments)
            {
                if (p.Key == null)
                    continue;

                strings.Add(p.Key);
                strings.Add(p.Value);
            }
            return type + string.Join("_", strings);
        }
        static readonly ConcurrentDictionary<string, CallSite<Func<CallSite, object, object>>> _getters =
            new ConcurrentDictionary<string, CallSite<Func<CallSite, object, object>>>();

        static readonly ConcurrentDictionary<Type, Func<object, object>> _converters =
            new ConcurrentDictionary<Type, Func<object, object>>();
        static Func<object, object> CompileConverter(Type targetType)
        {
            var valueParameter = Expression.Parameter(typeof(object), "value");

            return Expression.Lambda<Func<object, object>>(
                Expression.Convert(
                    Expression.Dynamic(
                        Microsoft.CSharp.RuntimeBinder.Binder.Convert(CSharpBinderFlags.ConvertExplicit, targetType, null),
                        targetType,
                        valueParameter),
                    typeof(object)),
                valueParameter).Compile();
        }

While I didn't have access to the context they used in the ShapeAttribute strategy, I found I could just as easily bind to the shape I was given to get the parameters for the shape method. Now to implement the actual caching code:

public void Discover(ShapeTableBuilder builder)
        {
            foreach (var occurence in _occurrences)
            {
                var shape = occurence.MethodInfo.GetCustomAttributes(typeof(ShapeAttribute), false).OfType<ShapeAttribute>().FirstOrDefault();
                if (shape != null)
                {
                    var shapeType = shape.ShapeType ?? occurence.MethodInfo.Name;
                    CacheAttributeOccurence occurence1 = occurence;
                    builder.Describe(shapeType)
                        .OnDisplaying(displaying =>
                        {
                            var cacheContext = new CacheContext { MethodInfo = occurence1.MethodInfo, CacheHit = false };
                            displaying.Shape._cacheContext = cacheContext;
                            string key = ConstructCacheKey(shapeType, displaying.Shape, cacheContext).ToString();
                            if (!string.IsNullOrWhiteSpace(key)) {
                                var result = _cache.TryGet(key);
                                if (result != null)
                                {
                                    displaying.ChildContent = result;
                                    cacheContext.CacheHit = true;
                                }
                            }
                        })
                        .OnDisplayed(displayed =>
                        {
                            var cacheContext = displayed.Shape._cacheContext as CacheContext;
                            if (cacheContext == null)
                                return;
                            if (cacheContext.CacheHit)
                            {
                                return;
                            }
                            string key = ConstructCacheKey(shapeType, displayed.Shape, cacheContext).ToString();
                            if (!string.IsNullOrWhiteSpace(key))
                            {

                                _cache.Get(key, acquire =>
                                {
                                    acquire.Monitor(Clock.When(TimeSpan.FromSeconds(occurence1.CacheAttribute.Duration)));
                                    return displayed.ChildContent;
                                });

                            }
                        });
                }
            }
        }

You'll notice in my code that I'm using a method that doesn't exist in the default cache implementation: .TryGet(). The reason being is that .Get() forces you to have a way to populate the cache at execution time if the item doesn't exist in cache, which I'm unable to do so because the shape hasn't been displayed yet. Instead I had to suppress the default Orchard CacheHolder and construct my own:

 [OrchardSuppressDependency("Orchard.Caching.DefaultCacheHolder")]
    public class CacheHolder : ICacheHolder
    {
        private readonly ConcurrentDictionary<CacheKey, object> _caches = new ConcurrentDictionary<CacheKey, object>();
        class CacheKey : Tuple<Type, Type, Type>
        {
            public CacheKey(Type component, Type key, Type result)
                : base(component, key, result)
            {
            }
        }
        public ICache<TKey, TResult> GetCache<TKey, TResult>(Type component) {
            var cacheKey = new CacheKey(component, typeof(TKey), typeof(TResult));
            var result = _caches.GetOrAdd(cacheKey, k => new ACLJ.Caching.Cache<TKey, TResult>());
            return (Cache<TKey, TResult>)result;
        }
    }
public class Cache<TKey, TResult> : ICache<TKey, TResult>
    {
        private readonly ConcurrentDictionary<TKey, CacheEntry> _entries;

        public Cache()
        {
            _entries = new ConcurrentDictionary<TKey, CacheEntry>();
        }
        public TResult TryGet(TKey key) {
            CacheEntry value = null;
            var entry = _entries.TryGetValue(key, out value);
            if (value == null)
                return default(TResult);

            return value.Result;
        }
        public TResult Get(TKey key, Func<AcquireContext<TKey>, TResult> acquire)
        {
            var entry = _entries.AddOrUpdate(key,
                // "Add" lambda
                k => CreateEntry(k, acquire),
                // "Update" lamdba
                (k, currentEntry) => (currentEntry.Tokens.All(t => t.IsCurrent) ? currentEntry : CreateEntry(k, acquire)));

            // Bubble up volatile tokens to parent context
            if (CacheAquireContext.ThreadInstance != null)
            {
                foreach (var token in entry.Tokens)
                    CacheAquireContext.ThreadInstance.Monitor(token);
            }

            return entry.Result;
        }

        private static CacheEntry CreateEntry(TKey k, Func<AcquireContext<TKey>, TResult> acquire)
        {
            var entry = new CacheEntry { Tokens = new List<IVolatileToken>() };
            var context = new AcquireContext<TKey>(k, volatileItem => entry.Tokens.Add(volatileItem));

            IAcquireContext parentContext = null;
            try
            {
                // Push context
                parentContext = CacheAquireContext.ThreadInstance;
                CacheAquireContext.ThreadInstance = context;

                entry.Result = acquire(context);
            }
            finally
            {
                // Pop context
                CacheAquireContext.ThreadInstance = parentContext;
            }
            return entry;
        }

        private class CacheEntry
        {
            public TResult Result { get; set; }
            public IList<IVolatileToken> Tokens { get; set; }
        }
    }

    /// <summary>
    /// Keep track of nested caches contexts on a given thread
    /// </summary>
    internal static class CacheAquireContext
    {
        [ThreadStatic]
        public static IAcquireContext ThreadInstance;
    }

Voila. Now I can just apply my CacheAttribute to any shape methods I want to cache like so:


        [Shape, Cache]
        public IHtmlString ContentImage(dynamic Display, ContentItem item, string suffix) {
            var image = item.As<ImageAttachmentPart>();

            if (image != null && image.Image != null)
            {
                var imageUrl = ImageService.GetUrl(image.Image.ContentItem.As<ImagePart>(), suffix);
                var dim = ImageService.GetDimensions().Where(d => d.Suffix == suffix).First();
                return Display.ImageView(ImageUrl: imageUrl, Width: dim.Width, Height: dim.Height);
            }

            return T("");
        }

And the result of this shape method will be stored in the cache based on the parameters specified to the shape method.

My next step is to make the parameter analysis when constructing the cache key a little bit more foolproof, as it won't handle any complex types. I'd also like to add some options to the CacheAttribute that allow it to specify which parameters should be used to construct the cache key.

I'm also trying to think of how to easily cache a shape that is defined only in a razor view, so if any one has any ideas on how to go about that, hit me up. If I can solve those problems, I'll release the module to the community.

15Feb/116

Orchard Shape Wizardry

While working on my employer's new website, we discovered that we had a need that didn't seem to fit into the Widget system for Orchard. Several places throughout our site, we need to be able to trigger the creation of a shape from a template, but have that shape populated with data through a controller of sorts. After a lengthy discussion with Bertrand Le Roy (who is ever so helpful), I stumbled upon the solution that we were looking for: IShapeTableProvider.

Inside of my template somewhere, I can make a call like so:
WorkContext.Layout.BeforeHeader.Add(New.HeaderBar(), "1");
This creates a new shape called HeaderBar and adds it to the BeforeHeader zone I have configured in our theme. Elsewhere (we have it in a common module we use to contain widgets & shapes that may be site-wide but don't make sense to keep in the template), I can create a shape called HeaderBar.cshtml and the shape factory will automatically use that Razor template to display the shape. However, now we need to provide data to the shape.

To do so, I created a ShapeProviders namespace in our common module and added the following class:

public class HeaderBar : IShapeTableProvider {
        private readonly IWorkContextAccessor _workContextAccessor;
        private readonly IHttpContextAccessor _httpContextAccessor;

        // not injected the usual way because this component is a 'static' dependency and any services that consume data are per-request
        private ICaseService CaseService
        {
            get
            {
                return _workContextAccessor.GetContext(_httpContextAccessor.Current()).Resolve<ICaseService>();
            }
        }
        public HeaderBar(IWorkContextAccessor workContextAccessor, IHttpContextAccessor httpContextAccessor)
        {
            _workContextAccessor = workContextAccessor;
            _httpContextAccessor = httpContextAccessor;
        }
        public void Discover(ShapeTableBuilder builder)
        {
            builder.Describe("HeaderBar").OnCreated(created =>
            {
                created.Shape.Cases = CaseService.GetFeaturedCases().Slice(0,4).ToList();                

            });
        }
    }

So what this is doing is describing the HeaderBar shape that we're using, and hooking into the OnCreated event for this shape. This allows us to call into a service and grab the data we need to populate the shape and assign it to the shape.

One thing I need to note, and this took me an entire day to discover, is that you can't just inject your data service into your IShapeTableProvider implementation. If you do, it'll try to use a transaction out of scope and that will cause you all kinds of problems. After tearing my hair out for hours on end, I finally discovered what the Orchard team is doing inside of the CoreShapes class: The trick to resolve a service dependency within the function itself by using a property that loads the service per-request.

Once I did that, I was able to use the data within my Razor view easily:

@using Orchard.ContentManagement;
@using Orchard.Core.Routable.Models;
@{
    var cases = Model.Cases;

}
Current Cases:
    @foreach (var c in cases) { var item = c as ContentItem; var route = item.As();
  • @route.Title
  • |
  • }
  • More...
Get Email Updates | Already Registered? Login