Thursday, 8 November 2018

Sitecore Config Layers and ConfigBuilders

Getting rid of transformations, allow the same build to run in any environment.

It has always been a hassle for me to manage web.config transforms, and slow cheetha transformations for config files. It is annoying to have your build servers do multiple builds to essentially have the same code compile with a different set of variables.

It is solved in Octopus Beploy, by having the build agent replacing tokens within the configuration. However a cleaner option was added in .net 4.7.1 .... tadaaa ConfigBuilders.

And while it opens up a whole set of options, like where to save your configuration and secrets, Azure Storage and similar comes to mind. A handy alternative is to use Environment variables, specially for Sitecore implementations, where Environment variables can be set locally for the application pool in the ApplicationHosts.

Luckily microsoft have an example of this in a preview nuget


Setting up the content builders in web.config..

Add config builders to config sections:
<configSections>
 <section name="configBuilders" type="System.Configuration.ConfigurationBuildersSection, System.Configuration, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" restartOnExternalChanges="false" requirePermission="false"/>
 </configSections>

Add a new section defining your environemnt builder:
<configBuilders>
  <builders>
     <add name="Environment" type="Microsoft.Configuration.ConfigurationBuilders.EnvironmentConfigBuilder, Microsoft.Configuration.ConfigurationBuilders.Environment" />
  </builders>
</configBuilders>

Example of the appSettings using the environment builder:
<appSettings configBuilders="Environment">
      <add key="role:define" value="SET env var" />
 <add key="env:define" value="Set env var" />
 <add key="search:define" value="Solr" />
</appSettings>

Monday, 8 October 2018

Sitecore and dev#lang cookie

I observed what seemed to be a bit strange behavior in a Sitecore 7.2 implementation, I reasontly joined. The strange behavior was only in one of my colleague dev environments (it works on my machine – or actually it, did but not on my colleague).

When the site tried to resolve some specific urls, the last part of the url (after the domain), was solved as the context language. This resulting in a cookie named dev#local was set with a value containing the last part of the url (Eg. If the url was www.mydomain.com/om-Godt the value of the dev#local cookie was set to “om-Godt”.



Since the language “om-Godt” didn’t exist, the site redirected to an error page. 


Funny though, because it was not all the time a page was rendered. Anyway, copying a language value into the url (www.mydomain.com/en/contact-us) the site resolved correctly. After clarifying this, it was pretty obvious where to go next. We verified the “languageEmbedding” was set to “Never” in the LinkManager setting. Just to make sure, it was handled consistently in the solution.

<add name="sitecore" type="Sitecore.Links.LinkProvider, Sitecore.Kernel" 
addAspxExtension="false" alwaysIncludeServerUrl="false" encodeNames="true" 
languageEmbedding="Never" languageLocation="filePath" lowercaseUrls="false" 
shortenUrls="true" useDisplayName="false" />


We located the Languages.AlwasyStripLanguage setting in the web.config file (it was a 7.2 solution) and verified the setting was set to true. We created an include file for the setting and set it to false.


      <!--  LANGUAGES ALWAYS STRIP LANGUAGE
      This setting specifies if the StripLanguage processor in the <preprocessRequest> pipeline will parse and remove languages from
      the URL, even when the languageEmbedding attribute of the linkProvider is set to "never". You should only change this setting 
      to "false" if the default behavior causes problems in your solution.
      Default value: true 
-->
      <setting name="Languages.AlwaysStripLanguage" value="true" />


After that the language was solved correctly though out the solution.

Tuesday, 19 June 2018

Sitecore Marketing Automation Activity Type not Showing?

Sitecore Marketing Automation Custom Activity type not showing in Marketing Automation Plan?

Ever tried to create a new Custom Acitivity type in Sitecore Marketing Automation. Followed the instructions at https://doc.sitecore.net/developers/xp/marketing-automation/activities/activity-types/index.html? And the activity type you created still doesn’t appear in the Marketing Actions chunk in the Toolbox Area in the right side of the Marketing Automation App?



Going through the documentation several times making sure, you have created everything right?

Well, if you select plugin class (the plugin containing the metadata that the Sitecore Marketing Automation application uses to find and register the activity type configured in the plugin). Make sure the ID not just match your custom Sitecore activity item ID in your plugin decorator property(located under “master:/sitecore/System/Settings/Analytics/Marketing Automation/Activity Types/My Custom Activity Type”), but keep the ID in lower case.


import { Plugin } from '@sitecore/ma-core';
import { SamplePluginActivity } from './SamplePlugin.Activity';
import { SampleSingleItemModuleNgFactory } from './codegen/sample-single-item.module.ngfactory';
import { SampleSingleItemEditorComponent } from './editor/sample-single-item-editor.component';
@Plugin({
   activityDefinitions: [
       {
           // The ID must match the ID of the activity type description definition item in the CMS.
           //BUT REMEMBER TO LOWERCASE()
           id: 'E24EB15B-1B20-45F2-A48F-FA8FD1549DC1'.toLowerCase(),
           activity: SamplePluginActivity,
           editorComponenet: SampleSingleItemEditorComponent,
           editorModuleFactory: SampleSingleItemModuleNgFactory
       }
   ]
})
export default class SamplePlugin {}

Monday, 26 March 2018

Sitecore 9.0.1 - Publishing Target

I’m currently working on an upgrade project – upgrading a Sitecore v 8.1 solution to Sitecore v9.0.1. I’m using Sitecore Update Installation Wizard. Sure, I had some preparations for the upgrade (like making clean Sitecore include config files etc.), but after that, the upgrade it self actually ran quite smoothly – anyway, better than my +13 years of Sitecore implementation would have predicted…

Sitecore v9. have some breaking changes, new ways of doing configurations etc.

One of the new stuff is how to add new Sitecore databases/Publishing Target. Still, you have to create a “Pulishing Target” item in the Sitecore Shell (located at “master:/sitecore/system/publishing targets”). Create a new Item based on the “Publishing Target” template and add the Database ID in the “Target database” field. Add the connectionstring to the “/App_Config/ConnectionString.config” file (name the connectionstring element with the value from the “Target database” field). But the database element in the Sitecore.Config file has changed. Sitecore no longer uses the proxyDataProvider element. Also, you must add settings for the propertystore and the eventqueueprovider

The Sitecore v8 database element in the Sitecore.config file goes from:
 ...
 <databases>
    <database id="publish-target-Name" singleInstance="true" type="Sitecore.Data.Database, Sitecore.Kernel">
      <param desc="name">$(id)</param>
      <icon>Images/database_web.png</icon>
      <securityEnabled>true</securityEnabled>
      <dataProviders hint="list:AddDataProvider">
        <dataProvider ref="dataProviders/main" param1="$(id)">
          <disableGroup>publishing</disableGroup>
          <prefetch hint="raw:AddPrefetch">
            <sc.include file="/App_Config/Prefetch/Common.config" />
            <sc.include file="/App_Config/Prefetch/Webdb.config" />
          </prefetch>
        </dataProvider>
      </dataProviders>
      <proxiesEnabled>false</proxiesEnabled>
      <proxyDataProvider ref="proxyDataProviders/main" param1="$(id)" />
      <archives hint="raw:AddArchive">
        <archive name="archive" />
        <archive name="recyclebin" />
      </archives>
      <cacheSizes hint="setting">
        <data>100MB</data>
        <items>50MB</items>
        <paths>2500KB</paths>
        <itempaths>50MB</itempaths>
        <standardValues>2500KB</standardValues>
      </cacheSizes>
    </database>
  </databases>


In Sitecore v9.0.1 To:
...
<databases>
    <database id="publish-target-Name" singleInstance="true" type="Sitecore.Data.DefaultDatabase, Sitecore.Kernel">
      <param desc="name">$(id)</param>
      <icon>Images/database_web.png</icon>
      <securityEnabled>true</securityEnabled>
      <dataProviders hint="list:AddDataProvider">
        <dataProvider ref="dataProviders/main" param1="$(id)">
          <disableGroup>publishing</disableGroup>
          <prefetch hint="raw:AddPrefetch">
            <sc.include file="/App_Config/Prefetch/Common.config" />
            <sc.include file="/App_Config/Prefetch/Webdb.config" />
          </prefetch>
        </dataProvider>
      </dataProviders>
      <PropertyStore ref="PropertyStoreProvider/store[@name='$(id)']" />
      <remoteEvents.EventQueue>
        <obj ref="eventing/eventQueueProvider/eventQueue[@name='$(id)']" />
      </remoteEvents.EventQueue>
      <archives hint="raw:AddArchive">
        <archive name="archive" />
        <archive name="recyclebin" />
      </archives>
      <cacheSizes hint="setting">
        <data>100MB</data>
        <items>50MB</items>
        <paths>2500KB</paths>
        <itempaths>50MB</itempaths>
        <standardValues>2500KB</standardValues>
      </cacheSizes>
    </database>
    </databases>


And add the propertyStoreProvider:
<PropertyStoreProvider>
    <store name="publish-target-Name" prefix="publish-target-Name" getValueWithoutPrefix="true" singleInstance="true" type="Sitecore.Data.Properties.$(database)PropertyStore, Sitecore.Kernel" >
      <param ref="dataApis/dataApi[@name='$(database)']" param1="$(name)" />
      <param resolve="true" type="Sitecore.Abstractions.BaseEventManager, Sitecore.Kernel" />
      <param resolve="true" type="Sitecore.Abstractions.BaseCacheManager, Sitecore.Kernel" />
    </store>
  </PropertyStoreProvider>
Add the EventpQueueProvider:
  <eventing>
    <eventQueueProvider>
      <eventQueue name="publish-target-Name" type="Sitecore.Data.Eventing.$(database)EventQueue, Sitecore.Kernel">
        <param ref="dataApis/dataApi[@name='$(database)']" param1="$(name)" />
        <param ref="PropertyStoreProvider/store[@name='$(name)']" />
      </eventQueue>
    </eventQueueProvider>
  </eventing>




Sunday, 22 October 2017

Sitecore Configuration Layer Config - Layers.config

In Sitecore v9, Sitecore divides configuration files into layers, which in its simplicity gives you the ability to control in which order, the config files are loaded. This is handled in a new config file named Layers.config. The Layers.config file is located at the website/App_config/Layers.config and contains four layers for areas in Sitecore to manage. The layers are:

  • Custom
  • Environment
  • Modules
  • Sitecore

The “Custom” layer is used for your custom implementation configuration files and customizing existing Sitecore standard configuration files. The Custom area folder in the App_Config is called “Include”.

The “Environment” layer is used for dividing configurations into environment specific configurations (Development, QA, Production etc).

The “Modules” layer is used for configuration files for Sitecore modules (EXM, Sitecore Publishing Service etc.).

The “Sitecore” layer is used for standard Sitecore Component configuration files (Content Search, Experience Analytics, Owin etc.).

In the Layers.config file each <layer> element has a “name” attribute and an “includeFolder” attribute defining the path to the configuration folder of the layer. The <layer> element contains a <loadOrder> element defining the load order of the configuration files specified for that specific layer. Each configuration element (patch file) is added into the <loadOrder> element and has a “path” attribute (defining the path to the configuration area) and a “type” attribute (defining whether it is a File or a Folder specific element).
<layers>
  <layer name="Sitecore" includeFolder="/App_Config/Sitecore/">
    <loadOrder>
      <add path="CMS.Core" type="Folder" />
      <add path="AntiCSRFModule" type="Folder" />
      …
    </loadOrder>
  </layer>
  <layer name="Custom" includeFolder="/App_Config/Include/" />
  …
</layers>

In case of <layer includefolder="/App_Config/Include/" name="Custom"> Sitecore loads all the configuration files placed in the “Include” folder. By default, Sitecore loads the configuration files as in previous versions of Sitecore (first, Sitecore reads the files in the root of the “Include” folder alphabetically. Secondly, Sitecore loads the configuration files alphabetically in order of the subfolders alphabetically). This, because no load order has been specified. 

If you want to specify the load order of the “Custom” area configuration folders and files located in the “Include” folder, you simply add the <loadOrder> element to the layer. From here, you specify the “path” and “type” attribute to the <add> element (the patch file) E.g.:

  <layer name="Custom" includeFolder=”/App_config/Include” >
    <loadOrder>
      <add path=”MySitecoreBaslineExtensions” type=”Folder” />
      <add path=”MyCustomSolutionHttpRequestBeginExtensions” type=”Folder” />
      <add path=”Folder1/MySitecoreProcessorExtension.config” type=”File” />
    </loadOrder>
  </layer>

After loading the configurations specified in the <loadOrder> element, Sitecore loads the remaining files and folders like Sitecore normally do.

From the Layers.config file, you can disable all patch files in a specific layer or you can disable a single file/folder. This is done by adding the mode=”off” attribute to the <layer> element or at the patch file element it self.

Disabling a layer:
<layer name="Custom" includeFolder=”/App_config/Include” mode=”off”>
  <loadOrder>
    <add path=”MySitecoreBaslineExtensions” type=”Folder” />
    <add path=”MyCustomSolutionHttpRequestBeginExtensions” type=”Folder” />
    <add path=”Folder1/MySitecoreProcessorExtension.config” type=”File” />
  </loadOrder>
</layer>

Disabling a file:
  <layer name="Custom" includeFolder=”/App_config/Include”>
    <loadOrder>
      <add path=”MySitecoreBaslineExtensions” type=”Folder” />
      <add path=”MyCustomSolutionHttpRequestBeginExtensions” type=”Folder” />
      <add path=”Folder1/MySitecoreProcessorExtension.config” type=”File” mode=”off” />
    </loadOrder>
  </layer>

The default load order is Sitecore, Modules, Custom and Environment. This load order should not be changed due potential conflicts.

If you need to customize existing Sitecore configuration files best practice is still to create your own configuration files. But now you can add/place it in the Custom or Environment layers of the Layers.config file.

The Layers.config file:
  
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <layers>
    <layer includefolder="/App_Config/Sitecore/" name="Sitecore">
      <loadOrder>
        <add path="CMS.Core" type="Folder" />
        <add path="AntiCSRFModule" type="Folder" />
        <add path="Owin" type="Folder" />
        <add path="Owin.Authentication" type="Folder" />
        <add path="ContentSearch" type="Folder" />
        <add path="ContentSearch.Azure" type="Folder" />
        <add path="Marketing.Xdb.Sql.Common" type="Folder" />
        <add path="XConnect.Client.Configuration" type="Folder" />
        <add path="Marketing.Xdb.ReferenceData.Service" type="Folder" />
        <add path="Marketing.Xdb.ReferenceData.SqlServer" type="Folder" />
        <add path="Marketing.Xdb.ReferenceData.Client" type="Folder" />
        <add path="Marketing.Operations.xMgmt" type="Folder" />
        <add path="Marketing.Operations.Xdb.ReferenceData" type="Folder" />
        <add path="Marketing.xDB" type="Folder" />
        <add path="Marketing.Tracking" type="Folder" />
        <add path="Marketing.Assets" type="Folder" />
        <add path="Tracking.Web.RobotDetection" type="Folder" />
        <add path="Mvc" type="Folder" />
        <add path="Tracking.Web.MVC" type="Folder" />
        <add path="ItemWebApi" type="Folder" />
        <add path="Buckets" type="Folder" />
        <add path="SPEAK" type="Folder" />
        <add path="Services.Client" type="Folder" />
        <add path="SPEAK.Components" type="Folder" />
        <add path="Marketing.Client" type="Folder" />
        <add path="Marketing.Xdb.MarketingAutomation.Reporting" type="Folder" />
        <add path="Marketing.Xdb.MarketingAutomation.Operations" type="Folder" />
        <add path="Marketing.Xdb.MarketingAutomation.Locators" type="Folder" />
        <add path="Marketing.Xdb.MarketingAutomation.Tracking" type="Folder" />
        <add path="XConnect.Segmentation.xMgmt" type="Folder" />
        <add path="Speak.Applications" type="Folder" />
        <add path="LaunchPad" type="Folder" />
        <add path="ListManagement" type="Folder" />
        <add path="Marketing.Automation.Client" type="Folder" />
        <add path="Marketing.Automation.ActivityDescriptors.Client" type="Folder" />
        <add path="ExperienceForms" type="Folder" />
        <add path="Experience Editor" type="Folder" />
        <add path="MVC.ExperienceEditor" type="Folder" />
        <add path="MVC.DeviceSimulator" type="Folder" />
        <add path="ContentTesting" type="Folder" />      
        <add path="ExperienceProfile" type="Folder" />
        <add path="Update" type="Folder" />
        <add path="Contact.Enrichment.Services.Client" type="Folder" />
        <add path="DetectionServices.Location" type="Folder" />
        <add path="FederatedExperienceManager" type="Folder" />
        <add path="DeviceDetection.Client" type="Folder" />
        <add path="ExperienceAnalytics" type="Folder" />
        <add path="CampaignCreator" type="Folder" />
        <add path="ExperienceContentManagement.Administration" type="Folder" />
      </loadOrder>
    </layer>
    <layer includefolder="/App_Config/Modules/" name="Modules" />
    <layer includefolder="/App_Config/Include/" name="Custom" />
    <layer includefolder="/App_Config/Environment/" name="Environment" />
  </layers>
</configuration>



Thursday, 5 October 2017

Sitecore Application_Start and <initialize> Pipeline

For some time ago, I needed to build up some cache on Sitecore application start up. Sure, in a clean web application I could use the Global.asax, but in the world of Sitecore, the Global.asax has been made private. Instead of changing the Global.asax file, you should use the initialize pipeline. The initialize pipeline is started at application start (you find the initialize pipeline in Sitecore.config).

In this case of building up some caching on Sitecore application start, I create a new config include file and add the my new processor:
 
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
<pipelines>
    <initialize>
      <processor type="MyExtensions.Pipelines.Caching.CachePrefill, MyExtensions" xdt:Transform="Insert" />
    </initialize>
  </pipelines>
</Sitecore>
</configuration>

Do the coding. Notice, you don’t need to inherit from any other namespaces. 
namespace MyExtensions.Pipelines.Caching
{
    public class CachePrefill
    {
        public void Process(PipelineArgs args)
        { 
            Sitecore.Diagnostic.Log.Info("MyExtensions: Running CachePrefill.Process method", this);
            
            //do my magic cachePrefill();
            
            Sitecore.Diagnostic.Log.Info("MyExtensions: Done running CachePrefill.Process method", this);
        }
    }
}

Nothing more, nothing less.

Thursday, 31 August 2017

Sitecore WFFM Create Custom Field

Creating custom WFFM Form field is simple and only requires three steps. In the following example, I just want to create a new Singleline field suggesting the country, where the visitor is located.

First you need to create a WFFM Field using the “/sitecore/templates/Web Forms for Marketers/Field Type” template. The new WFFM Field should be located at “/sitecore/system/Modules/Web Forms for Marketers/Settings/Field Types” in the Master database. In this case, I added at “/Custom”. Fill in the Assembly, the Class and the MVC type fields.



After creating the new Custom WFFM Field item, the field is visible at the Field Type dropdown list in the Form Designer:


Implement the codings. inherit from Sitecore.Form.Web.UI.Controls.SingleLineText, and override OnInit(EventArgs e):

namespace MyExtensions.Logic.FieldTypes.WFFM
{
    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.Text;
    using System.Web.UI.WebControls;
    using DSV.Cms.Logic.Helpers;

    using Sitecore.Data.Items;

    public class CountrySingleLineText : Sitecore.Form.Web.UI.Controls.SingleLineText
    {
        protected override void OnInit(EventArgs e)
        {
            this.textbox.CssClass = "scfSingleLineTextBox country";
            this.help.CssClass = "scfSingleLineTextUsefulInfo";
            this.generalPanel.CssClass = "scfSingleLineGeneralPanel";
            this.title.CssClass = "scfSingleLineTextLabel";
            this.textbox.TextMode = TextBoxMode.SingleLine;
            this.Controls.AddAt(0, this.generalPanel);
            this.Controls.AddAt(0, this.title);
            this.generalPanel.Controls.AddAt(0, this.help);
            this.generalPanel.Controls.AddAt(0, this.textbox);
            this.textbox.Attributes.Add("type", "text");

            this.textbox.Text = LanguageSuggestion();

            StringBuilder sbCountries = new StringBuilder();
            List countryNameList = GetCountryNames();
            sbCountries.Append("[");
            int index = 1;
            foreach (string country in countryNameList)
            {
                sbCountries.Append(string.Format("\"{0}\"", country));
                if (index != countryNameList.Count)
                {
                    sbCountries.Append(",");
                }
                index ++;
            }
            sbCountries.Append("]");
            this.textbox.Attributes.Add("data-countries", sbCountries.ToString());
        }

        private string LanguageSuggestion()
        {
            // Do your language look up
            return countryCode;
        }


        protected List GetCountryNames()
        {
            //Do your listing of the Country Item values and add them to list
            return countryItemList;
        }
    }
}