Monday, 20 February 2023

Running eclint locally in development environment

If you have come accross eclint in Azure Devops or in other scenarios, you might want to run this in local development instead to get your linting return an error, possible crashing the build pipeline in Azure Devops. Eclints checks the .editorconfig file of your VS solution and reports if your code is badly formatted according to this file code style rules. Here is a typical eclint setup written with YAML
 
 # Runs ECLINT CHECK. This verfies that code conforms to editor config file.
parameters:
  lintingResultArtifactName: 'LINTING' # Name of published artifact with results.

steps:
- task: Bash@3
  displayName: 'Template steps-linting'
  inputs:
    targetType: 'inline'
    script: 'echo Template steps linting'
- task: Bash@3
  displayName: 'Install ECLINT'
  inputs:
    targetType: 'inline'
    script: |
        sudo npm install -g eclint
    failOnStderr: false
- task: Bash@3
  displayName: 'ECLINT check'
  inputs:
    targetType: 'inline'
    script: |
      eclint check $(git ls-files -- . ':!:*.sln' . ':!:*.sln.DotSettings') > $(Build.ArtifactStagingDirectory)/eclint.txt 2>&1
      file=$(Build.ArtifactStagingDirectory)/eclint.txt
      if [ -s "$file" ]
      then
        echo " ECLint reported errors - build failed 😔" 1>&2
        cat $file
        exit 1
      else
        echo " No errors found. You did good 👍 "
        exit 0
      fi
    failOnStderr: false
- task: PublishBuildArtifacts@1
  displayName: 'Publish artifact'
  condition: succeededOrFailed()
  inputs:
    PathtoPublish: '$(Build.ArtifactStagingDirectory)'
    ArtifactName: ${{ parameters.lintingResultArtifactName }}
    publishLocation: 'Container'


 
The following shell script can be run with Git Bash in for example Windows environments to run eclint locally.
 
 #!/bin/bash

GREEN='\033[0;32m'
RED='\033[0;31m'
NC='\033[0m' # No Color

exists() {
    command -v "$1" > /dev/null 2>&1
}

runEcLint(){
    echo -e "Starting up eclint linting the files of the solution! ${GREEN}[SUCCESS]${NC} Current folder:"
    pwd
    eclint check $(git ls-files -- . ':!:*.sln' . ':!:*.sln.DotSettings') > eclint.txt 2>&1
    file=eclint.txt
    if [ -s "$file" ]
    then
        echo " ECLint reported errors - build failed 😔" 1>&2
        cat $file
        echo -e "ECLint command reported a ${RED}[FAIL]${NC}. Correct the errors."
        rm eclint.txt
        exit 1
    else
        echo " No errors found. You did good 👍 "
        echo -e "ECLint command reported a ${GREEN}][SUCCESS]${NC}"
        rm eclint.txt
        exit 0
    fi
}

echo "Running eclint locally to check the solution. Checking LOCALDEV environment.."

echo "Checking that npm is installed globally.. "
if  ! [[ "exists npm" ]]; then
    echo -e "Npm is not installed globally. ${RED} [FAIL]${NC}"
    echo "Install npm globally first before running this script! See: https://nodejs.org/en/download/"
    echo "Recommended next step: Install LTS of jnodejs together with npm"
    exit 1
else
    echo -e "You have already installed npm (globally). ${GREEN} [SUCCESS]${NC}"
fi

echo "Checking that eclint is installed globally.. "
if  !  [[ "exists eclint" ]]; then
    echo -e "eclint is not installed globally. ${RED} [FAIL]${NC}"
    echo "Make sure you run this script with sufficient access : Attempting to install eclint globally next."
    echo "Trying to run this command to install eclint: npm install eclint -g"
    npm install -g eclint
    echo -e "\neclint should now be installed. Continuing! ${GREEN} [SUCCESS]${NC}"
else
    echo -e "You have already installed eclint (globally). ${GREEN} [SUCCESS]${NC}"
fi

echo "Switching up a folder to run at root folder of the source.."
pushd ..
echo -e "Switched to parent folder ${GREEN}[SUCCESS]${NC}"

runEcLint

popd #back to the eclint folder
  
 
In case you are running WSL, chances are that you must check if a command is available like this: if ! [ -x "$(command -v npm)" ]; then Also, you might need to add 'sudo' before the npm install -g command. The following launch.json file shows how you can debug inside VsCode Bash scripts, using extension Bash Debug for VsCode.
 
 {
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "type": "bashdb",
            "request": "launch",
            "name": "Bash-Debug",
            "cwd": "${workspaceFolder}",
            "program": "${file}",
            "args": []
            }
    ]
}
 
I tested the script against a solution at work and it spotted the intended eclint warnings for multiple files, as a proof of concept. This makes it easier to fix up eclint errors before pushing them to Azure Devops !

Sunday, 5 February 2023

Calculating the weeks difference in C#

This short article will present a method to calculate a WeekDiff method in C#. We could first just look at the TimeSpan and divide by seven to get the amount of whole weeks two dates differs. Lets first implement it like this :
 
 
         /// <summary>
        /// Calculates the number of weeks between <paramref name="fromDate"/> and <paramref name="toDate"/>.
        /// Also, the remainder days are looked upon and calculation will also see if weeks are different by adding fromDate with the remainding days
        /// (i.e. the difference in total days minus N full weeks.
        /// The weeks ago is the number of 'full weeks' plus an extra week if there are more days and the week numbers
        /// are different between fromDate and fromDate+remainding days giving in some calculation N+1 weeks ago if the week numbers are different by adding the remainder.
        /// </summary>
        /// <param name="fromDate">The from date to calculate the number of weeks diff</param>
        /// <param name="toDate">The to date to calculate the number of weeks diff</param>
        /// <param name="startOfWeek">Start of week (DayOfWeek). Default will be Monday. For english culture, use Sunday.</param>
        /// <param name="currentCulture">Specify culture, this will in turn use the Calendar of the culture for calculation. Fallbacks to CurrentCulture if not specified.</param>
        /// <returns></returns>
        public static int CalcWeekDiff(this DateTime fromDate, DateTime toDate)
        {
            if (fromDate > toDate)
            {
                var temp = fromDate;
                fromDate = toDate;
                toDate = temp;
            }
            if (currentCulture == null)
            {
                currentCulture = CultureInfo.CurrentCulture;
            }
            TimeSpan diff = toDate.Subtract(fromDate);
            int weeks = diff.Days / 7;           
            return weeks;
        } 
 
But we could also look at the remainder part and include it also. Maybe two dates differs in 10 days, that is more than one week. If we qualify this as 'two weeks' we get:
 
 
        /// <summary>
        /// Calculates the number of weeks between <paramref name="fromDate"/> and <paramref name="toDate"/>.
        /// Also, the remainder days are looked upon and calculation will also see if weeks are different by adding fromDate with the remainding days
        /// (i.e. the difference in total days minus N full weeks.
        /// The weeks ago is the number of 'full weeks' plus an extra week if there are more days and the week numbers
        /// are different between fromDate and fromDate+remainding days giving in some calculation N+1 weeks ago if the week numbers are different by adding the remainder.
        /// </summary>
        /// <param name="fromDate">The from date to calculate the number of weeks diff</param>
        /// <param name="toDate">The to date to calculate the number of weeks diff</param>
        /// <param name="startOfWeek">Start of week (DayOfWeek). Default will be Monday. For english culture, use Sunday.</param>
        /// <param name="currentCulture">Specify culture, this will in turn use the Calendar of the culture for calculation. Fallbacks to CurrentCulture if not specified.</param>
        /// <returns></returns>
        public static int CalcWeeksDiff(this DateTime fromDate, DateTime toDate)
        {
            if (fromDate > toDate)
            {
                var temp = fromDate;
                fromDate = toDate;
                toDate = temp;
            }
            if (currentCulture == null)
            {
                currentCulture = CultureInfo.CurrentCulture;
            }
            TimeSpan diff = toDate.Subtract(fromDate);
            int weeks = diff.Days / 7;
            int remainingDays = diff.Days % 7;           
        }

 
We might also look more into looking at the week number of the dates. Then we must also consider CultureInfo and CalendarWeekRule - lets choose FirstFullWeek and what is the start of week in the calender which differs also from country to country. If we judge two dates then then to have different week numbers by adding the remainder part (i.e. diff.Days % 7) to give different week numbers, we add +1 to the final result. This should then work in special cases, such as years having 52 and 53 weeks around New Year. The weeks diff calculation then looks like this:
 
 
         /// <summary>
        /// Calculates the number of weeks between <paramref name="fromDate"/> and <paramref name="toDate"/>.
        /// Also, the remainder days are looked upon and calculation will also see if weeks are different by adding fromDate with the remainding days
        /// (i.e. the difference in total days minus N full weeks.
        /// The weeks ago is the number of 'full weeks' plus an extra week if there are more days and the week numbers
        /// are different between fromDate and fromDate+remainding days giving in some calculation N+1 weeks ago if the week numbers are different by adding the remainder.
        /// </summary>
        /// <param name="fromDate">The from date to calculate the number of weeks diff</param>
        /// <param name="toDate">The to date to calculate the number of weeks diff</param>
        /// <param name="startOfWeek">Start of week (DayOfWeek). Default will be Monday. For english culture, use Sunday.</param>
        /// <param name="currentCulture">Specify culture, this will in turn use the Calendar of the culture for calculation. Fallbacks to CurrentCulture if not specified.</param>
        /// <returns></returns>
        public static int CalcWeeksDiff(this DateTime fromDate, DateTime toDate, DayOfWeek startOfWeek = DayOfWeek.Monday, CultureInfo currentCulture = null)
        {
            if (fromDate > toDate)
            {
                var temp = fromDate;
                fromDate = toDate;
                toDate = temp;
            }
            if (currentCulture == null)
            {
                currentCulture = CultureInfo.CurrentCulture;
            }
            TimeSpan diff = toDate.Subtract(fromDate);
            int weeks = diff.Days / 7;
            int remainingDays = diff.Days % 7;
            var cal = currentCulture.Calendar;
            var fromDateWeekNo = cal.GetWeekOfYear(fromDate, CalendarWeekRule.FirstFullWeek, startOfWeek);
            var fromDateWeekNoPlusRemDays = cal.GetWeekOfYear(fromDate.AddDays(remainingDays), CalendarWeekRule.FirstFullWeek, startOfWeek);

            if (fromDateWeekNo != fromDateWeekNoPlusRemDays)
            {
                weeks++; //if week numbers are different, count +1 weeks (i.e. week diff does not have to be N 'full weeks', only that week numbers are different)
            }
            return weeks;
        }
 
 
Then we can check if for example 28th of December 2022 and 2nd of January 2023 are one week between and they are.
 
 	var from = new DateTime(2022, 12, 28); 
	var to = new DateTime(2023, 1, 2); 
	var weeksDiff = from.CalcWeeksDiff(to);
	weeksDiff.Dump("# of Weeks ago"); //Linqpad util method
 
So calculating weeks diff could take different approaches :
  • Number of weeks differing meaning integral part of weeks : just do a TimeSpan (swap if to is before from date) and divide by 7
  • Include considering if we have more than a integral part of weeks : consider also the remainder part - if remainder gives modulo % 7 > 0 we add +1 to the result
  • We have seen that we should include more checking of the remainder part to consider if two dates have different week numbers. Then also include CalendarWeek rules, CultureInfo and DayOfWeek

Friday, 3 February 2023

Looking into a stacktrace to find a given method name

When you log exceptions, it is sometimes interesting to look after a method name in the stackTrace (the 'callstack' where the application or system is running) An example usage of helper method I wrote to find the StackFrame containing the method of name to search is shown below.
 
     StackFrameExtensions.MethodStackFrameInfo methodStackFrame = stackTrace.FindStackFrameWithMethod("service", 2, nameof(OnEntry).ToLower(), nameof(LogUnauthorizedAccessDetails).ToLower());
 
This specifies that we want to look in the stack trace after a method which is containing 'service' (case insensitive), with a given minimum frame count of at least 2 from the place you retrieve the stackTrace and ignoring the method name "OnEntry" if it is found (Case insensitive). I have used this inside Postsharp aspects. It can be helpful in code where you either use AOP or some code where you expect a method call exist with a method name containing some string with a given distance (frame count) from the location in code you retrieve the stack frames. Note that you can always obtain a StackTrace by just instantiating a new StackTrace, e.g :
 
  var stackTrace = new StackTrace(); 
 
The stack trace helper extension method then looks like this:
 
 using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;

namespace DebuggingExtensionsLib
{
    public static class StackFrameExtensions
    {
        /// <summary>
        /// Retrieves the first stack frame of interest matching having a method with a name containing the <paramref name="methodNameContaining"/>. Case-insensitive search.
        /// </summary>
        /// <param name="stackTrace"></param>
        /// <param name="methodNameContaining">Pass in the method name to search for (case insensitive match)</param>
        /// <param name="minimumFrameCount">The minimum stack frame count. Defaults to 2.</param>
        /// <param name="ignoreMethodNamesContaining">Pass in one or more method names to ignore</param>
        /// <returns></returns>
        public static MethodStackFrameInfo FindStackFrameWithMethod(this StackTrace stackTrace, string methodNameContaining, int minimumFrameCount = 2, params string[] ignoreMethodNamesContaining)
        {
            try
            {
                var stackFrames = stackTrace.GetFrames();

                if (stackFrames == null || stackFrames.Length < minimumFrameCount)
                {
                    return null;
                }

                for (int i = 0; i < stackFrames.Length; i++)
                {
                    MethodBase mi = stackFrames[i].GetMethod();
                    if (mi.ReflectedType == null)
                    {
                        continue;
                    }
                    if (ignoreMethodNamesContaining != null && ignoreMethodNamesContaining.Any())
                    {
                        if (ignoreMethodNamesContaining.Contains(mi.Name, StringComparer.CurrentCultureIgnoreCase))
                        {
                            continue;
                        }
                    }
                    // Looks like the parameter value is not possible to obtain
                    string fullMethodName = $"{mi.ReflectedType.Name}.{mi.Name}";

                    if (!fullMethodName.Contains(methodNameContaining, StringComparison.CurrentCultureIgnoreCase))
                    {
                        continue;
                    }
                   
                    var parameterDictionary = mi.GetParameters().Select(mp => new
                    {
                        ParameterName = mp.Name,
                        ParameterType = mp.ParameterType.Name
                    }).ToDictionary(x => x.ParameterName, x => x.ParameterType);

                    var stackFrameInfo = new MethodStackFrameInfo
                    {
                        MethodName = fullMethodName,
                        MethodParameters = parameterDictionary
                    };
                    return stackFrameInfo;
                }

                return null;
            }
            catch (Exception err)
            {
                Debug.WriteLine(err);
                return null;
            }
        }

        public class MethodStackFrameInfo
        {
            public string MethodName { get; set; }

            public Dictionary<string, string> MethodParameters { get; set; } = new Dictionary<string, string>();

            public override string ToString()
            {
                return $"{MethodName}({string.Join(",", MethodParameters.Select(x => x.Value + " " + x.Key).ToArray())})";
            }
        }
    }
}
 
We return here an instance of MethodStackFrameInfo, where we get the method name and also the parameters given into the method with the value and the key, this means in this context the data type and the parameter name given as method parameter. It will return text like SomeClass.SomeMethod(Int32 somearg1, System.String somearg2). Hence you can log this information in a logger to understand which method was called, up the stack with a known name. This can be practical also in systems where you have some wrapping code and the code you want to inspect if was called is some stack frames up the stack.