Thursday, 23 July 2020

Helix Check - GitHub action

Recently I've prepared a GitHub action that checks solution structure to see if it is Helix compliant and follow guidelines of folders structure.

It is available on GitHub marketplace and ready to be used with Sitecore projects following Helix principles - https://github.com/marketplace/actions/helix-check.


If you're not familiar with GitHub actions you can read more about it here https://github.com/features/actions or in technical documentation - https://docs.github.com/en/actions.

General rules

Action tries to reflect Helix rules from documentation - https://helix.sitecore.net/

Projects references:
  • Feature layer projects can only reference Foundation
  • Foundation layer projects can only reference other Foundations
  • Project layer projects can reference Feature and Foundation but not other Projects
Folders structure and naming convention:
  • There are layer folders specified in the solution
    • Feature
    • Foundation
    • Project
  • Projects are placed in correct folders, for example:
    • src\Feature\ORM\website\Helixbase.Foundation.ORM.csproj - incorrect
    • src\Foundation\ORM\website\Helixbase.Foundation.ORM.csproj - correct

    Configuration

    Example workflow:

    name: Helix Check
    
    on:
      push:
        branches: [ develop, master ]
      pull_request:
        branches: [ develop, master ]
    
    jobs:
      check_job:
        name: Helix check
        runs-on: ubuntu-latest
    
        steps:
          - name: Checkout
            uses: actions/checkout@v2
    
          - name: Helix Check
            uses: ethisysltd/helix-check@v1.0
            id: check
            with:
              solution-file: 'Helixbase.sln'
              project-name: 'Helixbase'
              website-folder: 'website'
          
          - name: Get the check result
            run: echo "Check result - ${{ steps.check.outputs.result }}"
          
          - name: Get the output time
            run: echo "The time was - ${{ steps.check.outputs.time }}"

    It should be placed inside folder .github\workflows\ and named something like helix-check.yml

    Action is configured by these inputs:

    Input Description Usage
    solution-file Path to the solution that will be analyzed. Required
    project-name The name of your project. Required
    website-folder The name of the folder that always contain website project file.
    Default "website", earlier "code" was used.
    Optional

    While using the action makes sure you've checked the readme.md inside repo, as it will always be most up to date - https://github.com/ethisysltd/helix-check.

    In the action repository I've put example Helix solutions for tests - Helixbase by Neil Shack.
    If there are no issues found, simple log message is shown.

    Solution file: example-solution/valid/Helixbase.sln
    Project name: Helixbase
    Solution file exists.
    
    Solution is Helix compliant.


    I've managed to add a copy of that one and mess it up really badly so you can see result of analysis:

    Solution file: example-solution/invalid/Helixbase-Invalid.sln
    Project name: Helixbase
    Solution file exists.
    
    ##[warning]Issues with project Helixbase.Foundation.ORM
     Folder incorrect: src\Feature\ORM\code\Helixbase.Foundation.ORM.csproj
    
    ##[warning]Issues with project Helixbase.Feature.Hero
     Folder incorrect: src\Foundation\Hero\code\Helixbase.Feature.Hero.csproj
    
    ##[warning]Issues with project Helixbase.Project.Helixbase
     Incorrect references:
      - Helixbase.Project.Common
    
    ##[warning]Issues with project Helixbase.Feature.VersionTrim
     Incorrect references:
      - Helixbase.Project.Common
      - Helixbase.Feature.ShowTitles
    
    ##[warning]Issues with project Helixbase.Foundation.Core
     Incorrect references:
      - Helixbase.Feature.Redirects
      - Helixbase.Project.Common

    ##[error]Solution is not Helix compliant.

    Action is released to the Marketplace under https://github.com/marketplace/actions/helix-check. I will be adding additional checks soon, feel free to give me a shout if you find any issues or bugs.

    Cheers!

    Monday, 25 November 2019

    Azure Search - Improving speed while getting facets only

    If you want to get only facets from your search query - for some purposes as preparing filters list - remember to set limit to 0.

    For example like this:

    var facetResults = searchQuery.Take(0).GetFacets();

    .Take(0) translates to $top=0 in Azure Search query. That will return something like this:

    {
        "@odata.context": "https://{YOUR-SEARCH-SERVICE}.search.windows.net/indexes('my-index-web-index-99')/$metadata#docs(*)",
        "@odata.count": 857,
        "@search.facets": {
            "contenttype_facet_1": [
                {
                    "count": 146,
                    "value": "Content Type 1"
                },
                {
                    "count": 134,
                    "value": "Content Type 2"
                },
                {
                    "count": 118,
                    "value": "Content Type 3"
                },
            ],
            "topic_facet_1": [
                {
                    "count": 176,
                    "value": "Topic 1"
                },
                {
                    "count": 70,
                    "value": "Topic 2"
                },
                {
                    "count": 62,
                    "value": "Topic 3"
                },
                {
                    "count": 52,
                    "value": "Topic 4"
                }
            ],
            "subtopic_facet_1": [
                {
                    "count": 49,
                    "value": "Subtopic 1"
                },
                {
                    "count": 32,
                    "value": "Subtopic 2"
                }
            ]
        },
        "value": []
    }
    

    So only essential statistics data without actual results

    Friday, 1 November 2019

    RenderField processor for tooltip links

    A couple of days ago I've got a requirement from my client about inline tooltips functionality. Those tooltips needed to have formatted text so RTE field should be used.


    I started with creating repository folder for those tooltips




    Template for tooltip item was really simple, just one RTE field



    To have a link which will open a tooltip I had to write a pipeline processor which
    • Will read RTE field content
    • Find internal links
    • Check if they point to tooltip item
      • If yes then process link



    This pipeline processor must be run after field value is read, so after
    Sitecore.Pipelines.RenderField.GetFieldValue, Sitecore.Kernel

    Sunday, 20 October 2019

    Helix Publishing Pipeline - file locks issue on deploy

    On my last project when I was implementing Helix Publishing Pipeline I found really bizarre issue. Almost every time I was deploying code by publishing it from Visual Studio, some dlls were locked by w3wp process.

    No surprise IIS uses those assemblies but I didn't have such locking problem earlier.

    I was digging for hours in the Internet and I found out that IIS uses assembly shadowing. So basically it doesn't use directly dlls from webroot bin, but from its the temp folder.

    It can be disabled by this setting

    <hostingEnvironment shadowCopyBinAssemblies="false" />

    When I set this for test, file locks were all over the place.

    Earlier I used Process Explorer and I saw that w3wp locks not only shadowed dlls but as well those from webroot bin, so something was not right there!



    More digging eventually gave me information that our issue was self-inflicted: https://stackoverflow.com/a/28019475

    In GlassMapperScCustom assemblies were loaded by invoking method
    Assembly.LoadFile(assemblyPath) 

    which loads exact assemblies and it doesn't take into account shadows.

    Instead of that we should always use

    Assembly.LoadFrom(assemblyPath) 

    which loads shadows as it should be.

    That fixed issue with file locks for good!

    Tuesday, 19 February 2019

    Azure Search provider - Results ordering issue

    I was implementing Azure Search lately into my last project and I found some strange issue in the provider (we're using Sitecore 9.0.2 rev. 180604). When I wanted to order my search results using Linq like this:

    searchQuery.OrderBy(x => x.JobRole).ThenBy(x => x.LastName)

    it was translated to:

    $orderby=last_name_s,job_role_i

    So it looks like it's taking argument of ThenBy as first and OrderBy as the second. Inverting parameters:

    searchQuery.OrderBy(x => x.LastName).ThenBy(x => x.JobRole)

    gave me desired query:

    $orderby=job_role_i,last_name_s

    And search result as well were sorted like I wanted.

    Azure Search documentation states nothing unusual, but I had to confirm this:


    $orderby=[string] (optional)

    A list of comma-separated expressions to sort the results by. When calling via POST, this parameter is named orderby instead of $orderby. Each expression can be either a field name or a call to the geo.distance() function. Each expression can be followed by asc to indicate ascending, and desc to indicate descending. The default is ascending order. Ties will be broken by the match scores of documents. If no $orderby is specified, the default sort order is descending by document match score. There is a limit of 32 clauses for $orderby.
    https://docs.microsoft.com/en-us/rest/api/searchservice/search-documents#orderbystring-optional

    I have raised an issue on Sitecore Helpdesk and guys from support checked and reported the issue. It has a reference number 309333. Current resolution is to invert parameters like I did.


    Sunday, 2 December 2018

    Unicorn - disabling automatic items serialization

    I've come across a problem with Unicorn last time. We deployed to QA environment and a couple days later testers started to see issues like this:

    Unicorn: You cannot have a sparse serialized tree

    It was appearing when they were saving edited content. I've decided that we don't really need automatic items serialization on QA and other environments, so it will be best to just switch it off. I took a look into Unicorn.DataProvider.config and the comment there states:

    This file configures the Unicorn data provider. The data provider writes updated serialized items to disk when they are changed.
    This file should be removed in ANY deployed instance (CE or CD) that does not act as a source for serialized item updates.
    Generally speaking that's anywhere other than a developer workstation, so your CI process (you have one, right?) should remove this file during the build.
    IMPORTANT EXCEPTION: If you are using Transparent Sync as a deployment mechanism, this file must remain on your CE environment.

    That's exactly why it should be turned off on any non-development environments.

    As we are using Sitecore 9, we have set localenv variable to Local on our dev instances Web.config

    <add key="localenv:define" value="Local" />

    Instead of removing Unicorn.DataProvider.config config file after deployment, I have added localenv:require="Local" to root element there:

    <sitecore localenv:require="Local">
      <dataProviders>
        <!--
          Register the Unicorn data provider for use. If a database hooks to the Unicorn data provider it will
          automatically write changes to the database that match any configured predicate into the serialization provider.
     
          Changes that only affect Revision, Modified, or any fields ignored by FieldPredicates will be ignored.
        -->
        <unicorn type="Unicorn.Data.DataProvider.Unicorn$(database)DataProvider, Unicorn">
          <param connectionStringName="$(1)"/>
          <Name>$(1)</Name>
        </unicorn>
      </dataProviders>
     
      <!--
        Hook the Unicorn Data Provider into the master and core databases. If you're not
        syncing anything in core you can safely unregister it from here. If you want to
        sync something to another database register it here.
     
        It's safe to remove this config section on any environment where you are not
        collecting item changes, which may mean anywhere other than local development
        sites. This will avoid any performance hit from writing unused serialized files.
      -->
      <databases>
        <database id="master">
          <dataProviders>
            <dataProvider ref="dataProviders/main">
              <patch:attribute name="ref">dataProviders/unicorn</patch:attribute>
            </dataProvider>
          </dataProviders>
        </database>
        <database id="core">
          <dataProviders>
            <dataProvider ref="dataProviders/main">
              <patch:attribute name="ref">dataProviders/unicorn</patch:attribute>
            </dataProvider>
          </dataProviders>
        </database>
      </databases>
    </sitecore>

    That makes sure the changes made by editors won't be serialized.

    Saturday, 17 November 2018

    Renderings static binding

    Last time I had to prepare a component which behaves like a carousel of specific type of renderings. Instead of copy-pasting the Razor markup or using basic Razor partials I decided to use static binding of renderings - method I've never used before. It appeared to be extremely handy.

    Basically you can specify a rendering like this:

    @Html.Sitecore().Rendering("{ rendering ID }", new { Cacheable = true, Cache_VaryByData = true, DataSource = "{ DataSource ID}" })

    First parameter is rendering ID, second is anonymous object where we can specify additional properties, like caching or even datasource that will be used there.

    You can even add your custom properties and read them inside rendering, just like this:

    @Html.Sitecore().Rendering(
        Constants.MyRendering.RenderingId.ToString("B"),
        new
        {
            Cacheable = true,
            Cache_VaryByData = true,
            DataSource = myDatasource.Id.ToString("B"),
            MyProperty = "My custom value"
     })

    To be more familiar with possible properties of that anonymous object I have decompiled GetRendering method from SitecoreHelper class:


    I have created and extension method to easily retrieve rendering properties:

    And you can use it in view like this:

    var property = Html.Sitecore().GetRenderingProperty("MyProperty");