Thursday, 18 November 2010

EKTRON: Diagnosing 8.0 Extension Strategy Loading Failures

I’ve recently hit a problem with deploying  Ektron Extensions (the 8.0 replacement for Plugins), essentially every time my Extension ‘Strategy’ was called the workarea would display an error similar to:

Value cannot be null. Parameter name: type

Fortunately, in the Application Event log (for the CMS400 Source) there’s a bit more information:

   1: Timestamp: 18/11/2010 18:43:01
   2: Message: Could not load the MartinOnDotNet.Helpers.Ektron.Extensions.Monitoring.UserMonitoringStrategy, MartinOnDotNet.Helpers.Ektron.Extensions.Monitoring implementation from the configuration file
   3: Category: Error
   4: Priority: -1
   5: EventId: 0
   6: Severity: Error

This shows that the declaration in the web.config is correct:

   1: <objectFactory>
   2:     <objectStrategies>
   3:         <add name="User">
   4:             <strategies>
   5:                 <add name="UserMonitoringStrategy"
   6:                          type="MartinOnDotNet.Helpers.Ektron.Extensions.Monitoring.UserMonitoringStrategy, MartinOnDotNet.Helpers.Ektron.Extensions.Monitoring"/>
   7:             </strategies>
   8:         </add>
   9:     </objectStrategies>
  10:     <objectImplementations>
  11:     </objectImplementations>
  12: </objectFactory>

Fortunately, the next logged error reveals another vital clue:

   1: Timestamp: 18/11/2010 18:43:22
   2: Message: Exception thrown from: /WorkArea/login.aspx
   3: Value cannot be null.
   4: Parameter name: type   at System.Activator.CreateInstance(Type type, Boolean nonPublic)
   5:    at Ektron.Cms.ObjectFactory.GetStrategyList[T](String objectName)
   6:    at Ektron.Cms.User.EkUser.get_StrategyList()
   7:    at Ektron.Cms.User.EkUser.OnBeforeLogin(UserData userData)
   8:    at Ektron.Cms.User.EkUser.logIn(String username, String Password, String ServerName, String Domain, String Protocol, Boolean autologin, AutoAddUserTypes AutoAddType, Boolean ForceLogin)    at System.Activator.CreateInstance(Type type, Boolean nonPublic)
   9:    at Ektron.Cms.ObjectFactory.GetStrategyList[T](String objectName)
  10:    at Ektron.Cms.User.EkUser.get_StrategyList()
  11:    at Ektron.Cms.User.EkUser.OnBeforeLogin(UserData userData)
  12:    at Ektron.Cms.User.EkUser.logIn(String username, String Password, String ServerName, String Domain, String Protocol, Boolean autologin, AutoAddUserTypes AutoAddType, Boolean ForceLogin)
  13: Last 1 events
  14: [EventName]: [Timestamp] - ([UserID],[VisitorID])
  15: OnBeforeUserLogin: 18/11/2010 18:43:01 - (0, b7461b16-72f8-4631-b180-29e128d87184)
  16:  
  17: Category: Error
  18: Priority: -1
  19: EventId: 0
  20: Severity: Error

The reference to ‘Activator’ means that the issue is the .Net cannot find the referenced strategy through the type description.

This is a bit puzzling as the declaration is correct, so I need a bit more information on what’s causing the problem and why I can’t see it in the log, so that means  breaking out Reflector and digging into the API.  Fortunately, the problem method (GetStrategyList<T>) is easy to find and fairly simple to understand.  Here’s the code that’s failing:

   1: try
   2: {
   3:     type = Type.GetType(element2.Type, true);
   4: }
   5: catch (TypeLoadException)
   6: {
   7:     EkException.WriteToEventLog(string.Format("Could not load the {0} implementation from the configuration file", element2.Type), EventLogEntryType.Error);
   8: }
   9: strategy = (T) Activator.CreateInstance(type);    

Essentially, the first log entry is written in response to the actual problem (but doesn’t record it!) and the second, more detailed error is caused by the failure to assign the type variable.

What we really need to get hold of is the original TypeLoadException and capture the reason for the failure. There are two ways to do this:

  1. You could use the Visual Studio Exceptions dialog (Debug->Exceptions and check the ‘Thrown’ checkbox for System.TypeLoadException under ‘Common Language Runtime Exceptions) and use the debugger to inspect the error:image
  2. Create a test bed report to reproduce the strategy loading procedure with an increased amount of logging

My first brush attempt was using Option 1, but I found that it was a bit unreliable and would be limited to debugging my development machine.  The second option is more work, but is reusable and direct.  My weapon of choice for this type of report is a custom HttpHandler which can be added/removed through a simple configuration change:

   1: using System;
   2: using System.Configuration;
   3: using System.Web;
   4: using global::Ektron.Cms;
   5:  
   6: namespace MartinOnDotNet.Helpers.Ektron.Extensibilty
   7: {
   8:     /// <summary>
   9:     /// Diagnostic handler to test loading Extensions
  10:     /// </summary>
  11:     public class ValidateStrategiesHandler : IHttpHandler
  12:     {
  13:  
  14:         /// <summary>
  15:         /// Enables processing of HTTP Web requests by a custom HttpHandler that implements the <see cref="T:System.Web.IHttpHandler"/> interface.
  16:         /// </summary>
  17:         /// <param name="context">An <see cref="T:System.Web.HttpContext"/> object that provides references to the intrinsic server objects (for example, Request, Response, Session, and Server) used to service HTTP requests.</param>
  18:         public void ProcessRequest(HttpContext context)
  19:         {
  20:             context.Response.ContentType = "text/plain";
  21:             System.IO.TextWriter output = context.Response.Output;
  22:             output.WriteLine("Configured Extension Strategy Test\n");
  23:             ObjectFactorySection factorySection = (ObjectFactorySection)ConfigurationManager.GetSection("objectFactory");
  24:             foreach (ObjectStrategyConfigElement strategies in factorySection.ObjectStrategies)
  25:             {
  26:                 output.WriteLine("{0} (found {1})", strategies.Name, strategies.Strategies.Count);
  27:                 foreach (StrategyConfigElement strategy in strategies.Strategies)
  28:                 {
  29:                     RenderStrategyLoadingOutcome(output, strategy);
  30:                 }
  31:             }
  32:             output.WriteLine("\nLoaded Assemblies");
  33:             foreach (System.Reflection.Assembly ass in System.AppDomain.CurrentDomain.GetAssemblies())
  34:             {
  35:                 output.WriteLine(ass.FullName);
  36:             }
  37:  
  38:         }
  39:  
  40:         [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1804:RemoveUnusedLocals", MessageId = "type")]
  41:         private void RenderStrategyLoadingOutcome(System.IO.TextWriter output, StrategyConfigElement strategy)
  42:         {
  43:             output.Write("\t{0,-20}\t{1,-40}\t", strategy.Name, strategy.Type);
  44:             try
  45:             {
  46:                 Type type = Type.GetType(strategy.Type, true);
  47:                 Type baseType = type.BaseType;
  48:                 output.WriteLine("{0}\tOK!", baseType.Name);
  49:             }
  50:             catch (Exception tle)
  51:             {
  52:                 output.WriteLine("Failed! ({0}:{1})", tle.GetType().Name, tle.Message);
  53:                 System.Diagnostics.Trace.TraceError("Failed to Load Strategy '{0}'\n{1}", strategy.Type, tle.ToString());
  54:             }
  55:         }
  56:  
  57:        /// <summary>
  58:        /// Gets a value indicating whether another request can use the <see cref="T:System.Web.IHttpHandler"/> instance.
  59:        /// </summary>
  60:        /// <value></value>
  61:        /// <returns>true if the <see cref="T:System.Web.IHttpHandler"/> instance is reusable; otherwise, false.
  62:        /// </returns>
  63:        public bool IsReusable
  64:        {
  65:            // Return false in case your Managed Handler cannot be reused for another request.
  66:            // Usually this would be false in case you have some state information preserved per request.
  67:            get { return true; }
  68:        }
  69:     }
  70: }

This can simply be registered in the system.webServer/handlers section of the web.config (assuming IIS 7 and Integrated Pipeline):

   1: <add name="Verify Strategy Handler"
   2:      verb="GET"
   3:      path="secure/validatestrategies.ashx"
   4:      type="MartinOnDotNet.Helpers.Ektron.Extensibilty.ValidateStrategiesHandler"
   5:      preCondition="integratedMode,runtimeVersionv2.0" />

This report can now be called using /secure/validatestrategies.ashx">http://<website>/secure/validatestrategies.ashx and will render a report similar to:

   1: Configured Extension Strategy Test
   2:  
   3: Content (found 1)
   4:     LoggingStrategy          MartinOnDotNet.Helpers.Ektron.Extensibilty.LoggingContentStrategy, MartinOnDotNet.Helpers.Ektron    ContentStrategy    OK!
   5:  
   6: User (found 1)
   7:     UserMonitoringStrategy    MartinOnDotNet.Helpers.Ektron.Extensions.Monitoring.UserMonitoringStrategy, MartinOnDotNet.Helpers.Ektron.Extensions.Monitoring    Failed! (TypeLoadException:Could not load type MartinOnDotNet.Helpers.Ektron.Extensions.Monitoring.UserMonitoringStrategy' from assembly MartinOnDotNet.Helpers.Ektron.Extensions.Monitoring'.)
   8:  
   9: Loaded Assemblies
  10: ** Ommitted as there's hundreds **

With the full exception logged to Trace.  

What the report immediately showed was that the Extensions that had worked were in assemblies that were listed in the ‘Loaded Assemblies’ report.  The ones that failed weren’t.

This is odd as the library was definitely in the websites /bin folder, and a little more digging and investigation revealed that the one of the class libraries dependencies couldn’t be resolved correctly due to version number mismatch.  When that was fixed, the Strategies were loaded as expected!  Result.

   1: Configured Extension Strategy Test
   2:  
   3: Content (found 1)
   4:     LoggingStrategy          MartinOnDotNet.Helpers.Ektron.Extensibilty.LoggingContentStrategy, MartinOnDotNet.Helpers.Ektron    ContentStrategy    OK!
   5: User (found 1)
   6:     UserMonitoringStrategy    MartinOnDotNet.Helpers.Ektron.Extensions.Monitoring.UserMonitoringStrategy, MartinOnDotNet.Helpers.Ektron.Extensions.Monitoring    UserStrategy    OK!
   7:  
   8: Loaded Assemblies
   9: ** Ommitted as there's hundreds **

Hopefully, this report/test bench handler will help others with similar issues.

Wednesday, 3 November 2010

Performance: Applying Output Cache Policies to a User Controls

One of the great features in ASP.NET is the rich caching framework.  I particularly like being able to:

  1. Define caching policies in the configuration file and then assigning those policies to groups of pages (via the @OutputCache directive).
  2. Cache User Control output for reuse on multiple pages (Partial Page Caching)

What I don’t like is the fact that you can’t combine the two!  You have to hard code your user control cache directives either in the mark-up (as @OutputCache directive) or by applying an attribute (System.Web.UI.PartialCaching) to the code behind, both of which require at least the duration to be hardcoded.

This is a massive oversight, as best-practise ASP.Net dictates precompiling websites prior to production which means caching rules can’t be tweaked without frequent rebuild/deployment cycles.

Fortunately, we can work around the issue!

As with most of my workarounds, the trick is a new Extension Method to dynamically alter the UserControls Cache Policy:

   1: using System;
   2: using System.Linq;
   3: using System.Web.Configuration;
   4: using System.Web.UI;
   5:  
   6:  
   7: namespace MartinOnDotNet.Helpers.UI
   8: {
   9:     /// <summary>
  10:     /// Useful extension methods for Controls
  11:     /// </summary>
  12:     public static class ControlExtensions
  13:     {
  14:  
  15:  
  16:         private static OutputCacheSettingsSection _outputCacheSettings = ((OutputCacheSettingsSection)WebConfigurationManager.GetSection(@"system.web/caching/outputCacheSettings"));
  17:  
  18:         /// <summary>
  19:         /// Applies the cache policy.
  20:         /// </summary>
  21:         /// <param name="control">The control.</param>
  22:         /// <param name="cachePolicyName">Name of the cache policy.</param>
  23:         public static void ApplyCachePolicy(this UserControl control, string cachePolicyName)
  24:         {
  25:             if (control == null) throw new ArgumentNullException("control");
  26:             if (string.IsNullOrEmpty(cachePolicyName)) throw new ArgumentNullException("cachePolicyName");
  27:             if (_outputCacheSettings == null) return;
  28:             OutputCacheProfile profile = _outputCacheSettings.OutputCacheProfiles.OfType<OutputCacheProfile>()
  29:                 .Where(p => string.Equals(p.Name, cachePolicyName, StringComparison.OrdinalIgnoreCase))
  30:                 .FirstOrDefault();
  31:             if (profile == null || !profile.Enabled) return;
  32:             control.CachePolicy.SetExpires(DateTime.Now.AddSeconds(profile.Duration));
  33:             if (!string.Equals(profile.VaryByParam, "none", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrEmpty(profile.VaryByParam))
  34:                 foreach (string key in profile.VaryByParam.Split(new char[]{';'},  StringSplitOptions.RemoveEmptyEntries))
  35:                     control.CachePolicy.VaryByParams[key] = true;
  36:             else
  37:                 control.CachePolicy.VaryByParams.IgnoreParams = true;
  38:             if (!string.IsNullOrEmpty(profile.VaryByCustom)) control.CachePolicy.SetVaryByCustom(profile.VaryByCustom);
  39:             if (!string.IsNullOrEmpty(profile.VaryByControl)) control.CachePolicy.SetVaryByCustom(profile.VaryByControl);
  40:  
  41:  
  42:         }
  43:  
  44:     }
  45: }

To use it each user control will need to have a line added to the OnInit override in the codebehind (*.ascx.cs):

   1: using System;
   2: using MartinOnDotNet.Helpers.UI;
   3:  
   4: namespace MartinOnDotNet.Helpers.BrandingDemo
   5: {
   6:     /// <summary>
   7:     /// Example of a cached usercontrol
   8:     /// </summary>
   9:     public partial class CacheExample : System.Web.UI.UserControl
  10:     {
  11:         /// <summary>
  12:         /// Raises the <see cref="E:System.Web.UI.Control.Init"/> event.
  13:         /// </summary>
  14:         /// <param name="e">An <see cref="T:System.EventArgs"/> object that contains the event data.</param>
  15:         protected override void OnInit(EventArgs e)
  16:         {
  17:             base.OnInit(e);
  18:             if (this.CachePolicy.SupportsCaching)
  19:                 this.ApplyCachePolicy("Test");
  20:         }
  21:    
  22:     }
  23: }

The ascx file will need a place holder @outputcache directive:

   1: <%@ Control Language="C#" AutoEventWireup="true" CodeBehind="CacheExample.ascx.cs"
   2:     Inherits="MartinOnDotNet.Helpers.BrandingDemo.CacheExample" %>
   3:  
   4: <%@ OutputCache Duration="1" VaryByParam="none" Shared="true" %>
   5:  
   6: <% = DateTime.Now.ToString("G") %>

The values in the directive act as defaults and will be overridden by parameters in the matching Cache Policy (Test, in the code above), apart from Shared which isn’t included in the config.

Finally, define you cache policy in the web.config file:

   1: <system.web>
   2:     <caching>
   3:         <outputCacheSettings>
   4:             <outputCacheProfiles>
   5:                 <add name="Test"
   6:                          duration="3600"
   7:                          varyByParam="none" varyByCustom="DATE:yyyyMMddHHmm"/>
   8:             </outputCacheProfiles>
   9:         </outputCacheSettings>
  10:     </caching>
  11: </system.web>

The varyByCustom attribute in the rule above refers to some custom code in the Global.asax using the GetVaryByCustomString method.  In this case the code will vary by the current date in the given format making the cached version of the control rollover every minute.  This is a bit wasteful, as the cache will hang on to each version of the control for 3600 seconds (an hour) but proves the principle works.

Happily, the outputCacheSettings element of the web.config file supports configSource so you can actually apply different profiles for different environments by using an external config file.

Happy Caching!