Blog

Posts written in January 2017

Kentico Open Graph Custom Macro

Posted in: Kentico

Ever since I re-developed my website in Kentico 10 using Portal Templates, I have been a bit more daring when it comes to immersing myself into the inner depths of Kentico's API and more importantly - K# macro development. One thing that has been on my list of todo's for a long time was to create a custom macro extension that would render all required META open graph tags in a page.

Adding these type of META tags using ASPX templates or MVC framework is really easy to do when you have full control over the page markup. I'll admit, I don't know if there is already an easier way to do what I am trying to accomplish (if there is let me know), but I think this macro is quite flexible with capability to expand the open graph output further.

This is how I currently render the Meta HTML within my own website at masterpage level (click for a larger image):

Open Graph HTML In Masterpage

I instantly had problems with this approach:

  1. The code output is a mess.
  2. Efficiency from a performance standpoint does not meet my expectations.
  3. Code maintainability is not straight-forward, especially if you have to update this code within the Page Template Header content.

CurrentDocument.OpenGraph() Custom Macro

I highly recommend reading Kentico's documentation on Registering Custom Macro Methods before adding my code. It will give you more of an insight on what can be done that my blog post alone will not cover. The implementation of my macro has been developed for a Kentico site that is a Web Application and has been added to the "Old_App_Code" directory.

// Registers methods from the 'CustomMacroMethods' container into the "String" macro namespace
[assembly: RegisterExtension(typeof(CustomMacroMethods), typeof(TreeNode))]
namespace CMSApp.Old_App_Code.Macros
{
    public class CustomMacroMethods : MacroMethodContainer
    {
        [MacroMethod(typeof(string), "Generates Open Graph META tags", 0)]
        [MacroMethodParam(0, "param1", typeof(string), "Default share image")]
        public static object OpenGraph(EvaluationContext context, params object[] parameters)
        {
            if (parameters.Length > 0)
            {
                #region Parameter variables

                // Parameter 1: Current document.
                TreeNode tnDoc = parameters[0] as TreeNode;
                
                // Paramter 2: Default social icon.
                string defaultSocialIcon = parameters[1].ToString();

                #endregion

                string metaTags = CacheHelper.Cache(
                    cs =>
                    {
                        string domainUrl = $"{HttpContext.Current.Request.Url.Scheme}{Uri.SchemeDelimiter}{HttpContext.Current.Request.Url.Host}{(!HttpContext.Current.Request.Url.IsDefaultPort ? $":{HttpContext.Current.Request.Url.Port}" : null)}";

                        StringBuilder metaTagBuilder = new StringBuilder();

                        #region General OG Tags
                        
                        metaTagBuilder.Append($"<meta property=\"og:title\" content=\"{DocumentContext.CurrentTitle}\"/>\n");

                        if (tnDoc.ClassName == KenticoConstants.Page.BlogPost)
                            metaTagBuilder.Append($"<meta property=\"og:description\" content=\"{tnDoc.GetValue("BlogPostSummary", string.Empty).RemoveHtml()}\" />\n");
                        else
                            metaTagBuilder.Append($"<meta property=\"og:description\" content=\"{tnDoc.DocumentPageDescription}\" />\n");

                        if (tnDoc.GetValue("ShareImageUrl", string.Empty) != string.Empty)
                            metaTagBuilder.Append($"<meta property=\"og:image\" content=\"{domainUrl}{tnDoc.GetStringValue("ShareImageUrl", string.Empty).Replace("~", string.Empty)}?width=600\" />\n");
                        else
                            metaTagBuilder.Append($"<meta property=\"og:image\" content=\"{domainUrl}/{defaultSocialIcon}\" />\n");

                        #endregion

                        #region Twitter OG Tags

                        if (tnDoc.ClassName == KenticoConstants.Page.BlogPost || tnDoc.ClassName == KenticoConstants.Page.GenericContent)
                            metaTagBuilder.Append("<meta property=\"og:type\" content=\"article\" />\n");
                        else
                            metaTagBuilder.Append("<meta property=\"og:type\" content=\"website\" />\n");

                        metaTagBuilder.Append($"<meta name=\"twitter:site\" content=\"@{Config.Twitter.Account}\" />\n");
                        metaTagBuilder.Append($"<meta name=\"twitter:title\" content=\"{DocumentContext.CurrentTitle}\" />\n");
                        metaTagBuilder.Append("<meta name=\"twitter:card\" content=\"summary\" />\n");

                        if (tnDoc.ClassName == KenticoConstants.Page.BlogPost)
                            metaTagBuilder.Append($"<meta property=\"twitter:description\" content=\"{tnDoc.GetValue("BlogPostSummary", string.Empty).RemoveHtml()}\" />\n");
                        else
                            metaTagBuilder.Append($"<meta property=\"twitter:description\" content=\"{tnDoc.DocumentPageDescription}\" />\n");

                        if (tnDoc.GetValue("ShareImageUrl", string.Empty) != string.Empty)
                            metaTagBuilder.Append($"<meta property=\"twitter:image\" content=\"{domainUrl}{tnDoc.GetStringValue("ShareImageUrl", string.Empty).Replace("~", string.Empty)}?width=600\" />");
                        else
                            metaTagBuilder.Append($"<meta property=\"twitter:image\" content=\"{domainUrl}/{defaultSocialIcon}\" />");

                        #endregion

                        // Setup the cache dependencies only when caching is active.
                        if (cs.Cached)
                            cs.CacheDependency = CacheHelper.GetCacheDependency($"documentid|{tnDoc.DocumentID}");

                        return metaTagBuilder.ToString();
                    },
                    new CacheSettings(Config.Kentico.CacheMinutes, KenticoHelper.GetCacheKey($"OpenGraph|{tnDoc.DocumentID}"))
                );

                return metaTags;
            }
            else
            {
                throw new NotSupportedException();
            }
        }
    }
}

This macro has been tailored specifically to my site needs with regards to how I am populating the OG META tags, but is flexible enough to be modified based on a different site needs. I am carrying out checks to determine what pages are classed as "article" or "website". In this case, I am looking out for my Blog Post and Generic Content pages.

I am also being quite specific on how the OG Description is populated. Since my website is very blog orientated, there is more of a focus to populate the description fields with "BlogPostSummary" field if the current page is a Blog Post, otherwise default to "DocumentPageDescription" field.

Finally, I ensured that all article pages contained a new Page Type field called "ShareImageUrl", so that I have the option to choose a share image. This is not compulsory and if no image has been selected, a default share image you pass as a parameter to the macro will be used.

Using the macro is pretty simple. In the header section of your Masterpage template, just add the following:

Open Graph Macro Declaration

As you can see, the OpenGraph() macro can be accessed by getting the current document and passing in a default share icon as a parameter.

Macro Benchmark Results

This is where things get interesting! I ran both macro implementations through Kentico's Benchmark tool to ensure I was on the right track and all efforts to develop a custom macro extension wasn't all in vain. The proof is in the pudding (as they say!).

Old Implementation

Total runs: 1000
Total benchmark time: 1.20367s
Total run time: 1.20267s

Average time per run: 0.00120s
Min run time: 0.00000s
Max run time: 0.01700s

New Implementation - OpenGraph() Custom Macro

Total runs: 1000
Total benchmark time: 0.33222s
Total run time: 0.33022s

Average time per run: 0.00033s
Min run time: 0.00000s
Max run time: 0.01560s

The good news is that the OpenGraph() macro approach has performed better over my previous approach across all benchmark results. I believe caching the META tag output is the main reason for this as well as reusing the current document context when getting page values.

Salesforce .NET API: Select/Insert/Update Methods

Posted in: Salesforce

To continue my ever expanding Salesforce journey in the .NET world, I am adding some more features to my "ObjectDetailInfoProvider" class that I started writing in my previous post. This time making some nice easy, re-usable CRU(D) methods... just without the delete.

All the methods query Salesforce using Force.com Toolkit for .NET, which I have slightly adapted to allow me to easily interchange to a traditional REST approach when required.

Get Data

/// <summary>
/// Gets data from an object based on specified fields and conditions.
/// </summary>
/// <param name="objectName"></param>
/// <param name="fields"></param>
/// <param name="whereCondition"></param>
/// <param name="orderBy"></param>
/// <param name="max"></param>
/// <returns></returns>
public static async Task<List<dynamic>> GetRows(string objectName, List<string> fields, string whereCondition, string orderBy = null, int max = -1)
{
    ForceClient client = await AuthenticationResponse.ForceCom();

    #region Construct SQL Query

    StringBuilder query = new StringBuilder();

    query.Append("SELECT ");

    if (fields != null && fields.Any())
    {
        for (int c = 0; c <= fields.Count - 1; c++)
        {
            query.Append(fields[c]);

            query.Append(c != fields.Count - 1 ? ", " : " ");
        }
    }
    else
    {
        query.Append("* ");
    }

    query.Append($"FROM {objectName} ");

    if (!string.IsNullOrEmpty(whereCondition))
        query.Append($"WHERE {whereCondition} ");

    if (!string.IsNullOrEmpty(orderBy))
        query.Append($"ORDER BY {orderBy}");

    if (max > 0)
        query.Append($" LIMIT {max}");

    #endregion

    // Pass SQL query to Salesforce.
    QueryResult<dynamic> results = await client.QueryAsync<dynamic>(query.ToString());

    return results.Records;
}

Insert Row

/// <summary>
/// Creates a new row within an specific object.
/// </summary>
/// <param name="objectName"></param>
/// <param name="fields"></param>
/// <returns>Record ID</returns>
public static async Task<string> InsertRow(string objectName, Dictionary<string, object> fields)
{
    try
    {
        ForceClient client = await AuthenticationResponse.ForceCom();

        IDictionary<string, object> objectFields = new ExpandoObject();

        // Iterate through fields and populate dynamic object.
        foreach (KeyValuePair<string, object> f in fields)
            objectFields.Add(f.Key, f.Value);

        SuccessResponse response = await client.CreateAsync(objectName, objectFields);

        if (response.Success)
            return response.Id;
        else
            return string.Empty;
    }
    catch (Exception ex)
    {
        // Log error here.

        return string.Empty;
    }
}

Update Row

/// <summary>
/// Updates existing row within an specific object.
/// </summary>
/// <param name="recordId"></param>
/// <param name="objectName"></param>
/// <param name="fields"></param>
/// <returns>Record ID</returns>
public static async Task<string> UpdateRow(string recordId, string objectName, Dictionary<string, object> fields)
{
    try
    {
        ForceClient client = await AuthenticationResponse.ForceCom();

        IDictionary<string, object> objectFields = new ExpandoObject();

        // Iterate through fields and populate dynamic object.
        foreach (KeyValuePair<string, object> f in fields)
            objectFields.Add(f.Key, f.Value);

        SuccessResponse response = await client.UpdateAsync(objectName, recordId, objectFields);

        if (response.Success)
            return response.Id;
        else
            return string.Empty;
    }
    catch (Exception ex)
    {
        // Log error here.

        return string.Empty;
    }
}

The neat thing about Insert and Update methods is that I am using an ExpandoObject, which is a dynamic data type that can represent dynamically changing data. This is a new feature in .NET 4.0. Ideal for the ultimate flexibility when it comes to parsing field name and its value. It's a very dynamic object that allows you to add properties and methods on the fly and then access them again.

If there is any other useful functionality to add to these methods, please leave a comment.

Salesforce .NET API: Get Picklist Values

Posted in: Salesforce

I have been doing a lot of Saleforce integration lately, which has been both interesting and fun. Throughout my time working on Salesforce, I noticed that I am making very similar calls when pulling information out for consumption into my website. So I decided to make an extra effort to develop methods that would allow me to re-use commonly used functionality into a class library to make overall coding quicker.

I am adding all my Salesforce object query related functionality to a class object called "ObjectDetailInfoProvider". This will give me enough scope to expand with additional methods as I see fit. 

To start with, I decided to deal with returning all information from both picklist and multi-select picklists fields, since I find that I constantly require the values of data due to the vast number of forms I am developing. To be extra efficient in every request, I taken the extra step to cache all returned data for a set period of time. I hate the idea of constantly hammering away at an API unless absolutely necessary.

Before we get into it, it's worth noting that I am referencing a custom "AuthenticationResponse" class I created. You can grab the code here.

Objects

There are around seven class objects used purely for deserialization when receiving data from Salesforce. I'll admit I won't use all fields the API has to offer, but I normally like to have a complete fieldset to hand on the event I require further data manipulation.

The one to highlight out of all the class objects is "ObjectFieldPicklistValue", that will store key information about the picklist values, such as Label, Value and Active state. All methods will return this object.

public class ObjectFieldPicklistValue
{
    [JsonProperty("active")]
    public bool Active { get; set; }

    [JsonProperty("defaultValue")]
    public bool DefaultValue { get; set; }

    [JsonProperty("label")]
    public string Label { get; set; }

    [JsonProperty("validFor")]
    public string ValidFor { get; set; }

    [JsonProperty("value")]
    public string Value { get; set; }
}

I have added all other Object Field class objects to a snippets section on my Bitbucket account.

GetPicklistFieldItems() & GetMultiSelectPicklistFieldItems() Methods

Both methods perform similar functions; the only difference is cache keys and lambda expression to only pull out either a picklist or multipicklist by its field name.

/// <summary>
/// Gets a values from a specific picklist within a Salesforce object. Items returned are cached for 15 minutes.
/// </summary>
/// <param name="objectApiName"></param>
/// <param name="pickListFieldName"></param>
/// <returns>Pick list values</returns>
public static async Task<List<ObjectFieldPicklistValue>> GetPicklistFieldItems(string objectApiName, string pickListFieldName)
{
    string cacheKey = $"GetPicklistFieldItems|{objectApiName}|{pickListFieldName}";

    List<ObjectFieldPicklistValue> pickListValues = CacheEngine.Get<List<ObjectFieldPicklistValue>>(cacheKey);

    if (pickListValues == null)
    {
        Authentication salesforceAuth = await AuthenticationResponse.Rest();

        HttpClient queryClient = new HttpClient();

        string apiUrl = $"{SalesforceConfig.PlatformUrl}services/data/v37.0/sobjects/{objectApiName}/describe";

        HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, apiUrl);
        request.Headers.Add("Authorization", $"Bearer {salesforceAuth.AccessToken}");
        request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
                
        HttpResponseMessage response = await queryClient.SendAsync(request);

        string outputJson = await response.Content.ReadAsStringAsync();

        if (!string.IsNullOrEmpty(outputJson))
        {
            // Get all the fields information from the object.
            ObjectFieldInfo objectField = JsonConvert.DeserializeObject<ObjectFieldInfo>(outputJson);

            // Filter the fields to get the required picklist.
            ObjectField pickListField = objectField.Fields.FirstOrDefault(of => of.Name == pickListFieldName && of.Type == "picklist");
                    
            List<ObjectFieldPicklistValue> picklistItems = pickListField?.PicklistValues.ToList();

            #region Set cache

            pickListValues = picklistItems;

            // Add collection of pick list items to cache.
            CacheEngine.Add(picklistItems, cacheKey, 15);

            #endregion
        }
    }

    return pickListValues;
}

/// <summary>
/// Gets a values from a specific multi-select picklist within a Salesforce object. Items returned are cached for 15 minutes.
/// </summary>
/// <param name="objectApiName"></param>
/// <param name="pickListFieldName"></param>
/// <returns>Pick list values</returns>
public static async Task<List<ObjectFieldPicklistValue>> GetMultiSelectPicklistFieldItems(string objectApiName, string pickListFieldName)
{
    string cacheKey = $"GetMultiSelectPicklistFieldItems|{objectApiName}|{pickListFieldName}";

    List<ObjectFieldPicklistValue> pickListValues = CacheEngine.Get<List<ObjectFieldPicklistValue>>(cacheKey);

    if (pickListValues == null)
    {
        Authentication salesforceAuth = await AuthenticationResponse.Rest();

        HttpClient queryClient = new HttpClient();

        string apiUrl = $"{SalesforceConfig.PlatformUrl}services/data/v37.0/sobjects/{objectApiName}/describe";

        HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, apiUrl);
        request.Headers.Add("Authorization", $"Bearer {salesforceAuth.AccessToken}");
        request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

        HttpResponseMessage response = await queryClient.SendAsync(request);

        string outputJson = await response.Content.ReadAsStringAsync();

        if (!string.IsNullOrEmpty(outputJson))
        {
            // Get all the fields information from the object.
            ObjectFieldInfo objectField = JsonConvert.DeserializeObject<ObjectFieldInfo>(outputJson);

            // Filter the fields to get the required picklist.
            ObjectField pickListField = objectField.Fields.FirstOrDefault(of => of.Name == pickListFieldName && of.Type == "multipicklist");

            List<ObjectFieldPicklistValue> picklistItems = pickListField?.PicklistValues.ToList();

            #region Set cache

            pickListValues = picklistItems;

            // Add collection of pick list items to cache.
            CacheEngine.Add(picklistItems, cacheKey, 15);

            #endregion
        }
    }

    return pickListValues;
}

New Year, New Site!

This site has been longing for an overhaul, both visually and especially behind the scenes. As you most likely have noticed, nothing has changed visually at this point in time - still using the home-cooked "Surinder theme". This should suffice in the meantime as it currently meets my basic requirements:

  • Bootstrapped to look good on various devices
  • Simple
  • Function over form - prioritises content first over "snazzy" design

However, behind the scenes is a different story altogether and this is where I believe matters most. Afterall, half of web users expect a site to load in 2 seconds or less and they tend to abandon a site that isn’t loaded within 3 seconds. Damning statistics!

The last time I overhauled the site was back in 2014 where I took a more substantial step form to current standards. What has changed since then? I have upgraded to Kentico 10, but this time using ASP.NET Web Forms over MVC.

Using ASP.NET Web Form approach over MVC was very difficult decision for me. Felt like I was taking a backwards step in making my site better. I'm the kind of developer who gets a kick out of nice clean code output. MVC fulfils this requirement. Unfortunately, new development approach for building MVC sites from Kentico 9 onwards will not work under a free license.

The need to use Kentico as a platform was too great, even after toying with the idea of moving to a different platform altogether. I love having the flexibility to customise my website to my hearts content. So I had to the option to either refit my site in Kentico 10 or Kentico Cloud. In the end, I chose Kentico 10. I will be writing in another post why I didn't opt for the latter. I'm still a major advocate of Kentico Cloud and started using it on other projects.

The developers at Kentico weren't lying when they said that Kentico 10 is "better, stronger, faster". It really is! I no longer get the spinning loader for obscene duration of time whilst opening popups in the administration interface or lengthy startup times when the application has to restart.

Upgrading from Kentico 8.0 to 10 alone was a great start. I have taken some additional steps to keep my site clean as possible:

  1. Disable view state on all pages, components and user controls.
  2. Caching static files, such as CSS, JS and images. You can see how I do this at web.config level from this post.
  3. Maximising Kentico's cache dependencies to cache all data.
  4. Took the extra step to export all site contents into a fresh installation of Kentico 10, resulting in a slightly smaller web project and database size.
  5. Restructured pages in the content tree to be more efficient when storing large number of pages under one section. 

I basically carried out the recommendations on optimising website performance and then some! My cache statatics have never been so high!

My Kentico 10 Cache Statistics

One slight improvement (been a long time coming) is better open graph support when sharing pages on Facebook and Twitter. Now my links look pretty within a tweet.

;