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.

Monday, 23 January 2023

Creating a json string representing enum names and values in C#

A short blog post here demonstrating how to create a JSON string showing enum names and values for an enum in C#, simple stuff! From Linqpad 7:

void Main()
{
	var jsonEnum = EnumUtil.GenerateJsonForEnum<PasientOvertattEnum>();
	jsonEnum.Dump();	
}

public static class EnumUtil
{

	/// <summary>
	/// Generates a json string (array) for enum values
	/// Checked that it gives valid json array string here : https://jsonlint.com/
	/// </summary>
	public static string GenerateJsonForEnum<TEnum>()
	 where TEnum : struct, IConvertible
	{
		var enumItems = new List<object>();
		var sb = new StringBuilder();
		sb.AppendLine("[\n");
		bool isEnumValueFound = false;
		foreach (var enumValue in Enum.GetValues(typeof(TEnum)))
		{
			sb.AppendLine($@"	{{	""Name"": ""{enumValue}"", ""Value"": ""{(int)enumValue}""	}},");
			isEnumValueFound = true;
		}
		if (isEnumValueFound)
		{
			sb.Remove(sb.Length - 3, 1);
		}
		sb.AppendLine("\t]");
		return sb.ToString();
	}
}



This gives the following sample json string when testing:


[

  {  "Name": "Velgverdi", "Value": "0"  },
  {  "Name": "AkershusUniversitetssykehusHF", "Value": "1"  },
  {  "Name": "DiakonhjemmetSykehusAS", "Value": "2"  },
  {  "Name": "FinnmarkssykehusetHF", "Value": "3"  },
  {  "Name": "HaraldsplassDiakonaleSykehusAS", "Value": "4"  },
  {  "Name": "HelgelandssykehusetHF", "Value": "5"  },
  {  "Name": "HelseBergenHF", "Value": "6"  },
  {  "Name": "HelseFonnaHF", "Value": "7"  },
  {  "Name": "HelseFordeHF", "Value": "8"  },
  {  "Name": "HelseMoreogRomsdalHF", "Value": "9"  },
  {  "Name": "HelseNordTrondelagHF", "Value": "10"  },
  {  "Name": "HelseStavangerHF", "Value": "11"  },
  {  "Name": "LovisenbergDiakonaleSykehusAS", "Value": "12"  },
  {  "Name": "NordlandssykehusetHF", "Value": "13"  },
  {  "Name": "OsloUniversitetssykehusHF", "Value": "14"  },
  {  "Name": "SandvikaNevrosenter", "Value": "15"  },
  {  "Name": "StOlavshospitalHF", "Value": "16"  },
  {  "Name": "SykehusetiVestfoldHF", "Value": "17"  },
  {  "Name": "SykehusetInnlandetHF", "Value": "18"  },
  {  "Name": "SykehusetTelemarkHF", "Value": "19"  },
  {  "Name": "SykehusetOstfoldHF", "Value": "20"  },
  {  "Name": "SorlandetsykehusHF", "Value": "21"  },
  {  "Name": "UniversitetssykehusetNordNorgeHF", "Value": "22"  },
  {  "Name": "VestreVikenHF", "Value": "23"  },
  {  "Name": "Utenlands", "Value": "24"  },
  {  "Name": "Ukjent", "Value": "25"  }
  ]




The JSON string above has been tested and validated okay against : https://jsonlint.com/ So if you need to show the data contents of an enum into a Json, this is a simple way of doing this.

Saturday, 7 January 2023

List patterns in C# 11 - And getting a compiler error on code that should work

I have tested out list patterns in .NET 7 and C#. I am using Linqpad 7 and .NET 7.0.1. List patterns are useful I guess to compared sequences and fun to test out. The '_' discard here means to ignore the number at a given position and the '..' range here is to match anything between a given set of value and one index and then a given value at a higher index with arbitrary values between. But in one of the samples, it says you can capture variables inside list patterns. I cannot make it work, I get a compiler error. I am getting a CS0165 'Use of unassigned local variable' error when I try to access the variable(s) captured. I tried checking the crashing code also inside VsCode, still getting the error, however if I debug inside Linqpad I can see the variables that are captured got values at least.

    var someOddNumbers = new int[] { 1, 3, 5, 7, 9, 11 };
    bool resultX = someOddNumbers is [1, 3, _, _, _, 11];
    resultX.Dump("The 'someOddNumbers' equals a sequence of numbers 1,3,then three arbitrary numbers, then 11?");

    bool isOdd = someOddNumbers is [1, .., 9, 11];
    isOdd.Dump("The 'someOddNumbers' equals a sequence of numbers 1, some arbitrary numbers, then ending with 9 and 11?");
    
    result = input is [var firstOddNumber,.. var lastOddNumber];

    if (result)
    {
        Console.WriteLine($"The captured variables are: {firstOddNumber} and {lastOddNumber}"); //this lines gives the CS0165 error
    }

If I comment out the if block I can run the code sample, and in the debugger I can see firstOddNumber and lastOddNumber being set to a value at runtime. But the C# 11 compiler seems to think this is illeagal code since it is using an unintialized variable. I expected to not get a compiler error and be able to also capture the variables defined in the list pattern. I cannot understand the usage of such variables if I cannot use them. I understand that these variables might not be captured if the list pattern does not match, but even when checking if a match was present, I got the compilation error. I can however run the code, just not access the variables.

Sunday, 11 December 2022

Presentation in Norwegian presenting demo repository with GraphQL and Hotchocolate/Strawberryshake

I have written a presentation about GraphQL demo repository of mine here. It is in Norwegian, so it will not be translated to english here. For Norwegian readers : Jeg har skrevet en presentasjon pΓ₯ norsk om GraphQL som gΓ₯r igjennom et demo repository som benytter GraphQL, med HotChocolate i backend, sammen med Entity Framework Core 6 og .net 6 (C# selvsagt) og som i frontend benytter Blazor og StrawberryShake ! Dere kan lese de 43 slidesene vedlagt OneDrive lenken under. I foredraget gΓ₯r jeg igjennom key giveaways om GraphQL, inkludert case insensitive sΓΈk, omtale om paginerte data og projisering og gir et overblikk av hva GraphQL gΓ₯r ut pΓ₯ og hvilke fordeler man kan fΓ₯ ut av det. Sentrale fordeler med GraphQL er : * Fleksibilitet - spesifiser hvilke felter du vil ha for Γ₯ unngΓ₯ "overfetching" * -Ytelse - fΓ¦rre API kall og unngΓ₯r waterfall opphenting hvor man mΓ₯ hente opp stadig flere ressurser som i fra et REST API, men fΓ₯r en aggreggert tilpasset struktur av de data man faktisk vil ha tilbake * Ett felles endepunkt /graphql - man slipper Γ₯ lage controllere som i REST API - som ofte fΓΈles som unΓΈdvendig. GraphQL er ikke noe som kan lΓΈse alle utfordringer i API design, men det kan gi klienter mye mer fleksibilitet og ogsΓ₯ unngΓ₯ at API designere mΓ₯ stadig lage flere metoder og som har bΓ₯de "overfetching" eller enda verre - underfetching - som gir flere API kall og dΓ₯rligere ytelse. Man utnytter bΓ₯ndbredde og serverressurser bedre ved Γ₯ kun hente ut informasjon man trenger. Og GraphQL er ikke bare orientert rundt spΓΈrringer, men ogsΓ₯ endringer (mutations), pub sub event pattern (Subscriptions) og en hel del annen funksjonalitet som tilhΓΈrer API design ! Du kan lese Powerpoint presentasjonen her (43 slides, lesetid ca 1 time om du vil studere det nΓΈye, en 15 minutt om du vil skumlese mer). #blazor #hotchocolate #strawberryshake #chillicream #apidesign #csharp #dotnet #codinggrounds The presentation is here :

Powerpoint presentation (Norwegian, 11th december 2022) : https://1drv.ms/u/s!AhGGDxs-tzqJcFrls6Fue8Xnjx4?e=lWYYwU

Friday, 18 November 2022

Case insensitive search in HotChocolate GraphQL

I tested out the contains operator on string fields today in GraphQL. It is actually case sensitive, and this is counter-intuitive since I have connected the GraphQL database to a SQL database, which performs usually a case insensitive search with the 'contains' operator (using 'LIKE' operator
under the hood). The following adjustments need to be made to make it work : First off, define a class inheriting QueryableStringOperationHandler
 

using HotChocolate.Data.Filters;
using HotChocolate.Data.Filters.Expressions;
using HotChocolate.Language;
using System.Linq.Expressions;
using System.Reflection;

namespace AspNetGraphQLDemoV2.Server
{
    public class QueryableStringInvariantContainsHandler : QueryableStringOperationHandler
    {
        private static readonly MethodInfo _contains = typeof(string).GetMethod(nameof(string.Contains), new[] { typeof(string) })!;

        public QueryableStringInvariantContainsHandler(InputParser inputParser) : base(inputParser)
        {
        }

        protected override int Operation => DefaultFilterOperations.Contains;
        public override Expression HandleOperation(QueryableFilterContext context, IFilterOperationField field, 
            IValueNode value, object? parsedValue)
        {
            Expression property = context.GetInstance();
            if (parsedValue is string str)
            {
                var toLower = Expression.Call(property, typeof(string).GetMethod("ToLower", Type.EmptyTypes)!); //get the ToLower method of string class via reflection. The Type.EmptyTypes will retrieve the method overload of ToLower which accept no arguments.
                var finalExpression = Expression.Call(toLower, _contains, Expression.Constant(str.ToLower()));
                return finalExpression;

            }
            throw new InvalidOperationException();
        }
    }
}

 
We overload the Operation to 'Contains' so we are going to adjust how we treat the expression tree of GraphQL, overriding the HandleOperation. This is similar to the QueryableStringOperationHandler presented here: https://chillicream.com/docs/hotchocolate/api-reference/extending-filtering, in our case we support the contains method instead. The finalExpression 'DebugView' evaluates to :
.Call (.Call ($_s0.OfficialName).ToLower()).Contains("tind") To actually use this adaption of filtering of string properties in HotChocolate, we do the following in program.cs (startup class in .net 6) to not only add filtering support to HotChocolate, but also add a filter convention extension first to make it easier to register and avoid adding cluttering to the startup code in program.cs :

 
 using HotChocolate.Data.Filters;
using HotChocolate.Data.Filters.Expressions;

namespace AspNetGraphQLDemoV2.Server
{
    public class FilterConventionExtensionForInvariantContainsStrings : FilterConventionExtension
    {
        protected override void Configure(IFilterConventionDescriptor descriptor)
        {
            descriptor.AddProviderExtension(new QueryableFilterProviderExtension(
                                    y => y.AddFieldHandler<QueryableStringInvariantContainsHandler>()));
        }    
    }
}

 
You can see an example how I register this case insensitive contains filter in the program.cs example code below. Note both the usage of .AddFiltering() and the .AddConvention() call.
 
 
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("MountainsV2Db");
builder.Services
    .AddDbContext<MountainDbContext>(options =>
    {
        options.UseSqlServer(connectionString);
    })
    .AddCors()
    .AddGraphQLServer()
    .AddProjections()
    .AddFiltering()
    .AddConvention<IFilterConvention, FilterConventionExtensionForInvariantContainsStrings>()
    .AddSorting()
    .RegisterDbContext<MountainDbContext>()
    .AddQueryType<MountainQueries>()
    .AddMutationType<MountainMutations>()
    .AddSubscriptionType<MountainSubscriptions>()
    .AddInMemorySubscriptions();

var app = builder.Build();
 
Now, sample .graphql file (containing a GraphQL query) that shows how the new filtering capability can be used :
 
 query {
  mountains (where: { officialName: {contains: "TiND"}}) {
    officialName
  }
}
 
The backend code retrieves a list of data from a table and I have added the [UseFiltering] attribute to specify for HotChocolate that filtering should be supported to the method.
 
   public class MountainQueries
    {
        [UseFiltering]
        [UseSorting]
        public async Task<List<Mountain>> GetMountains([Service] MountainDbContext mountainDb)
        {
            return await mountainDb.Mountains.ToListAsync();
        }

//..

Sunday, 4 September 2022

Faster expansion of databases in Sql Server Management Studio

I have observed a slow expansion of databases tree in Sql Server Management Studio. This can actually be fixed on many Sql Server instances by running this T-SQL :
 
EXECUTE sp_MSforeachdb
'
IF (''?'' NOT IN (''master'', ''tempdb'', ''msdb'', ''model''))
   EXECUTE (''ALTER DATABASE [?] SET AUTO_CLOSE OFF WITH NO_WAIT'')'
 
 

Now, this does not look that much similar to SQL, since it is T-SQL. But it will execute to disable auto_close for every database on your database server (that is, 'instance') and this will make expanding databases in your database tree fast again in SSMS ! This is a variant of what Pinal Dave mentions here, I just made an iterative variant of what he suggests : https://blog.sqlauthority.com/2016/09/22/sql-server-set-auto_close-database-option-off-better-performance/

Saturday, 27 August 2022

DynamicObject : Thread safety and anonymous type initialization

This article will present code that shows how we can create a custom dynamic object and control its behavior, supporting thread safety when setting and getting members and allowing anonymous type initialization. The code is available in this GitHub repo : git clone https://github.com/toreaurstadboss/DynamicObjectThreadSafe.git The custom dynamic object inherits from the class DynamicObject and uses a thread safe dictionary.
 
 
 using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace DynamicObjectThreadSafe
{
	public class ThreadSafeDynamicObject : DynamicObject, IEnumerable<KeyValuePair<string, object>>
	{

		public ThreadSafeDynamicObject()
		{
		}

		public ThreadSafeDynamicObject(dynamic members)
		{
			dynamic membersDict = ToDictionary(members);
			InitMembers(membersDict);
		}

		private IDictionary<string, object> ToDictionary(object data)
		{
			var attr = BindingFlags.Public | BindingFlags.Instance;
			var dict = new Dictionary<string, object>();
			foreach (var property in data.GetType().GetProperties(attr))
			{
				if (property.CanRead)
				{
					dict.Add(property.Name, property.GetValue(data, null));
				}
			}
			return dict;
		}

		private void InitMembers(IDictionary<string, object> membersDict)
		{
			foreach (KeyValuePair<string, object> member in membersDict)
			{
				_members.AddOrUpdate(member.Key, member.Value, (key, oldValue) => member.Value);
			}
		}

		private readonly ConcurrentDictionary<string, object> _members = new ConcurrentDictionary<string, object>();

		public override bool TryGetMember(GetMemberBinder binder, out object result)
		{
			return _members.TryGetValue(binder.Name, out result);
		}

		public override bool TrySetMember(SetMemberBinder binder, object value)
		{
			_members.AddOrUpdate(binder.Name, value, (key, oldvalue) => value);
			return true;
		}

		public override IEnumerable<string> GetDynamicMemberNames()
		{
			return _members.Keys.ToList().AsReadOnly();
		}

		public override string ToString()
		{
			return JsonSerializer.Serialize(_members);
		}

		public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
		{
			return _members.GetEnumerator();
		}

		IEnumerator IEnumerable.GetEnumerator()
		{
			return _members.GetEnumerator();
		}
	}

}
 
 
The method ToDictionary transforms an input object, for example an anonymous class object via boxing (by accepting an object as parameter and implicitly 'boxing' it as a object) and the InitMember method will then populate the ConcurrentDictionary<string,object> object. This will allow us to pass anonymous objects and cast the object into a dynamic object, for further consumption. For example outputting fields of it. Now why would you use dynamic objects like this would you say? Dynamic objects are practical in many situations where you do not know the type until runtime. Many imlpementations of custom dynamic objects use Dictionary as a 'backing store' for the fields/properties/members of the object. This implementation uses ConcurrentDictionary and there should be thread safe concerning retrieving or setting members as shown in the overrides of methods TryGetMember and TrySetMember. The override for method GetDynamicMemberNames is for showing members in the debugger in the 'Dynamic view' to inspect the dynamic object properly. The GetEnumerator method overrides are to support casting the dynamic object to IDictionary<string,object> The following tests is then passing :
 
 
        [Fact]
        public void It_Can_Accept_Anonymous_Type_initialization()
        {
			dynamic threadSafeToyota = new ThreadSafeDynamicObject(new
			{
				Make = "Toyota",
				Model = "CR-H",
				Propulsion = new
				{
					IsHybrid = true,
					UsesPetrol = true,
					ElectricMotor = true
				}
			});

			Assert.Equal("Toyota", threadSafeToyota.Make);
			Assert.Equal("CR-H", threadSafeToyota.Model);
			Assert.Equal(true, threadSafeToyota.Propulsion.IsHybrid);
			Assert.Equal(true, threadSafeToyota.Propulsion.UsesPetrol);
			Assert.Equal(true, threadSafeToyota.Propulsion.ElectricMotor);
		}
 
 
And since this object is dynamic, we can extend it and adjust its members as dynamic allows you and as you can see we both can instantiate the dynamic object via anonymous type instance or populating it manually one property at a time. And doing so in a thread-safe many, for better support in multithreaded environments which are to expected many places today.

Monday, 22 August 2022

Splitting a ReadOnlySpan by a separator

This article will look into using the ReadonlySpan and doing an equivalent string split operation. The method uses ReadOnlySpan of type T (char) to split into 'words' or 'tokens', separated by a split char (separator). A span was introduced in C# 7.3 and can be used in .net standard 2.0 or .net framework using the Nuget package System.Memory.


  <ItemGroup>
    <PackageReference Include="System.Memory" Version="4.5.5" />
  </ItemGroup>
  

If you use newer target frameworks, you will get Span included as long as C# 7.3 is supported. The code here is just demonstration code, it successfully splits a ReadOnlySpan of T (char) using the contiguous memory on the stack, however, we must still convert to string here the 'tokens' or 'words' after the split operation. Note the usage of Slice method here to retrieve a range from the ReadOnlySpan of char here, we use a List of string to get the words or 'tokens'. It would be nice to somehow avoid string as much as possible, but we want to have an array of strings back anyways, so a List of string is used here to get the 'tokens'. What would be optimal would be to just return the split indexes as the code already extracts here and return those split indexes, which later could be used to build a string array. We have all the characters in the ReadOnlySpan of char here, so only having the split indexes would be sufficient. However, this would
from the consumer side be a bit cumbersome. You could though have a method like 'get nth word' using the split indexes here and so on.
 

using System;
using System.Collections.Generic;

namespace SpanStringSplit
{
    public static class SpanExtensions
    {

        public static string[] SplitViaSpan(this string input, char splitChar, StringSplitOptions splitOptions)
        {
            if (string.IsNullOrWhiteSpace(input) || input.IndexOf(splitChar) < 0)
            {
                return new string[] { input };
            }
            var tokens = SplitSpan(input.AsSpan(), splitChar, splitOptions);
            return tokens; 
        }

        public static string[] SplitSpan(this ReadOnlySpan<char> inputSpan, char splitChar, StringSplitOptions splitOptions)
        {
            if (inputSpan == null)
            {
                return new string[] { null };
            }
            if (inputSpan.Length == 0)
            {
                return splitOptions == StringSplitOptions.None ? new string[] { string.Empty } : new string[0]; 
            }
            bool isSplitCharFound = false; 
            foreach (char letter in inputSpan)
            {
                if (letter == splitChar)
                {
                    isSplitCharFound = true;
                    break;
                }
            }
            if (!isSplitCharFound)
            {
                return new string[] { inputSpan.ToString() }; 
            }

            bool IsTokenToBeAdded(string token) => !string.IsNullOrWhiteSpace(token) || splitOptions == StringSplitOptions.None;

            var splitIndexes = new List<int>();
            var tokens = new List<string>();
            int charIndx = 0;
            foreach (var ch in inputSpan)
            {
                if (ch == splitChar)
                {
                    splitIndexes.Add(charIndx);
                }
                charIndx++;
            }
            int currentSplitIndex = 0;
            foreach (var indx in splitIndexes)
            {
                if (currentSplitIndex == 0)
                {
                    string firstToken = inputSpan.Slice(0, splitIndexes[0]).ToString();
                    if (IsTokenToBeAdded(firstToken))
                    {
                        tokens.Add(firstToken);
                    }
                }
                else if (currentSplitIndex <= splitIndexes.Count)
                {
                    string intermediateToken = inputSpan.Slice(splitIndexes[currentSplitIndex - 1] + 1, splitIndexes[currentSplitIndex] - splitIndexes[currentSplitIndex - 1] - 1).ToString();
                    if (IsTokenToBeAdded(intermediateToken))
                    {
                        tokens.Add(intermediateToken);
                    }
                }
                currentSplitIndex++;
            }
            string lastToken = inputSpan.Slice(splitIndexes[currentSplitIndex - 1] + 1).ToString();
            if (IsTokenToBeAdded(lastToken))
            {
                tokens.Add(lastToken);
            }
            return tokens.ToArray();
        }

    }
}

 

And we have our suceeding unit tests :
 

using NUnit.Framework;
using System;

namespace SpanStringSplit.Test
{
    [TestFixture]
    public class SpanExtensionsSpec
    {
        [Test]
        public void SplitStringsViaSpan()
        {
            var tokens = ",,The,quick,brown,fox,jumped,over,the,lazy,,dog".SplitViaSpan(',', StringSplitOptions.RemoveEmptyEntries);
            CollectionAssert.AreEqual(new string[] { "The", "quick", "brown", "fox", "jumped", "over", "the", "lazy", "dog" }, tokens);
        }

        [Test]
        public void SplitStringsUsingSpan()
        {
            ReadOnlySpan<char> s = ",,The,quick,brown,fox,jumped,over,the,lazy,,dog".ToCharArray();                
            var tokens = s.SplitSpan(',', StringSplitOptions.RemoveEmptyEntries);
            CollectionAssert.AreEqual(new string[] { "The", "quick", "brown", "fox", "jumped", "over", "the", "lazy", "dog" }, tokens);
        }

    }
}

 
To sum up - we can use Span here to get an contiguous space of memory (stack in this case). To get a span from a string we use the extension method 'AsSpan()' To get a string from a range of a span (Slice), we just use the Slice method and then call ToString().
The following code then extracts the nth token (or word) only using the stack and first extracts the split indices (considering up to a given index + 1 of split indices if available) and then using the Splice method to get the chars of the token or 'word'.
 


        public static string GetNthToken(this ReadOnlySpan<char> inputSpan, char splitChar, int nthToken)
        {
            if (inputSpan == null)
            {
                return null;
            }
            int[] splitIndexes = inputSpan.SplitIndexes(splitChar, nthToken); 
            if (splitIndexes.Length == 0)
            {
                return inputSpan.ToString();
            }
            if (nthToken == 0 && splitIndexes.Length > 0)
            {
                return inputSpan.Slice(0, splitIndexes[0]).ToString(); 
            }
            if (nthToken > splitIndexes.Length)
            {
                return null; 
            }
            if (nthToken == splitIndexes.Length)
            {
                var split = inputSpan.Slice(splitIndexes[nthToken-1]+1).ToString();
                return split; 
            }
            if (nthToken <= splitIndexes.Length + 1)
            {
                var split = inputSpan.Slice(splitIndexes[nthToken-1]+1, splitIndexes[nthToken] - splitIndexes[nthToken-1]-1).ToString();
                return split; 
            }
            return null; 

        }

        public static int[] SplitIndexes(this ReadOnlySpan<char> inputSpan, char splitChar,
            int? highestSplitIndex = null)
        {
            if (inputSpan == null)
            {
                return Array.Empty<int>();
            }
            if (inputSpan.Length == 0)
            {
                return Array.Empty<int>();
            }
            bool isSplitCharFound = false;
            foreach (char letter in inputSpan)
            {
                if (letter == splitChar)
                {
                    isSplitCharFound = true;
                    break;
                }
            }
            if (!isSplitCharFound)
            {
                return Array.Empty<int>();
            }
         
            var splitIndexes = new List<int>();
            var tokens = new List<string>();
            int charIndex = 0;
            foreach (var ch in inputSpan)
            {
                if (ch == splitChar)
                {
                    if (highestSplitIndex.HasValue && highestSplitIndex + 1 < splitIndexes.Count)
                    {
                        break; 
                    }
                    splitIndexes.Add(charIndex);
                }
                charIndex++; 
            }
            return splitIndexes.ToArray(); 
        }
 
 
 
Now why would you use this instead of just sticking to ordinary string class methods. The main goal was to look into Span and how we can use it to look at contiguous memory and work at sub parts of this memory using the Slice method. In some applications, such as games and graphics in general, such micro optimizations are more important to avoid allocating a lot of string variables. Finding the split incides first (up to a given index if available) and then retrieving the nth token or word can be very useful instead of spitting into an array of strings. The unit tests are also passing for GetNthToken method :
 
        [Test]
        [TestCase(",, The, quick, brown, fox, jumped, over, the, lazy,, dog", 5, "fox")]
        [TestCase(",, The, quick, brown, fox, jumped, over, the, lazy,, dog", 0, "")]
        [TestCase(",, The, quick, brown, fox, jumped, over, the, lazy,, dog", 1, "")]
        [TestCase(",, The, quick, brown, fox, jumped, over, the, lazy,, dog", 2, "The")]
        [TestCase(",, The, quick, brown, fox, jumped, over, the, lazy,, dog", 3, "quick")]
        [TestCase(",, The, quick, brown, fox, jumped, over, the, lazy,, dog", 7, "over")]
        [TestCase(",, The, quick, brown, fox, jumped, over, the, lazy,, dog", 11, "dog")]
        [TestCase(",, The, quick, brown, fox, jumped, over, the, lazy,, dog", 12, null)]
        [TestCase(",, The, quick, brown, fox, jumped, over, the, lazy,, dog", 13, null)]
        public void GetNthWord(string input, int nthWord, string expectedWord)
        {
            ReadOnlySpan<char> s = ",,The,quick,brown,fox,jumped,over,the,lazy,,dog".ToCharArray();
            var word = s.GetNthToken(',', nthWord);
            Assert.AreEqual(word, expectedWord); 
        }

 

Sunday, 7 August 2022

Fiks for Tieto Min Arbeidsplan Auto-complete funksjon

Flere bruker Tieto Min Arbeidsplan pΓ₯ jobb i offentlig sektor. Dette produktet har en stor feil i seg nΓ₯r man skal redigere feks standardoppgaver. NΓ₯r man skal sΓΈke opp en prosjektkode og man har mange koder, sΓ₯ ser man problemet. I stedet for Γ₯ filtrere eller scrolle ned til riktig kode som
matcher det man har skrevet inn, sΓ₯ blir matchende elementer stylet med uthevet tekst (bold) og man scroller ikke. Dette er egentlig hΓ₯plΓΈst UI funksjonalitet. Her er en hotfix du kan gjΓΈre. 1. Trykk F12 i nettleseren for Γ₯ Γ₯pne UtviklingsverktΓΈy. Testet OK med Firefox, Edge Chromium og Chrome. 2. Velg fanen Konsoll / Console. 3. Lim sΓ₯ inn Javascript funksjonen her :
 
 


(function() {

document.getElementsByClassName("ui-select-search")[0].addEventListener("keydown", function(evt){
 var searchQueryText = evt.srcElement.value; 
 var rowsInSelect = document.getElementsByClassName("ui-select-choices-row");
 for (var i=0;i<rowsInSelect.length;i++) { 
    var rowInSelect = rowsInSelect[i];
    var targetInnerDiv = rowInSelect.querySelector('div');
    //debugger
    if (targetInnerDiv != null && i >= 0 && searchQueryText.length >= 3 && targetInnerDiv.textContent.toLowerCase().indexOf(searchQueryText.toLowerCase()) >= 0) { 
      rowInSelect.scrollIntoView();
      break;
    }   
 }     
});

})();  
 
Forklaring: Dette er en 'iffy', som er en Javascript funksjon som kaller seg selv etter Γ₯ ha blitt opprettet. Vi legger en til event listener pΓ₯ 'keydown' eventen for sΓΈkefeltet som har css klassen 'ui-select-search' (dvs. alle slike sΓΈkeelementer, vanligvis kun 1 sΓΈkefelt der man er inne pΓ₯ siden 'Rediger standardoppgaver'. NΓ₯r vi har 'keydown' og skriver pΓ₯ tastaturet sΓ₯ sΓΈker vi ogsΓ₯ opp alle elementer i DOM-en (Document Object Model, HTML-ens trestruktur av elementer/noder) som har
css klassen 'ui-select-choices-row'. SΓ₯ itererer vi vha en for-lΓΈkke alle elementene vi finner her og vi ser pΓ₯ children av hvert element sin div tag. Hvis vi finner en substring som matcher (case insensitivt)
og man har skrevet
tre tegn, sΓ₯ scroller man matchende rad element into view, altsΓ₯ slik at man scroller slik at matchende rad er synlig. Det er ikke altsΓ₯ lagt til noe filtrering her siden det ble litt mer kompleks patch, i stedet er dette en viktig scrolle fix sΓ₯ man slipper Γ₯ bruke masse tid pΓ₯ Γ₯ manuelt scrolle etter hvilken rad fikk styling med uthevet tekst. ForhΓ₯pentligvis fΓ₯r Tieto fikset denne feilen / bugen snart.

Sunday, 24 July 2022

Generic repository pattern for Azure Cosmos DB

I have looked into Azure Cosmos DB to learn a bit about this schemaless database in Azure cloud. It is a powerful 'document database' which saves and loads data inside 'containers' in databases in Azure. You can work against this database strongly typed in C# by for example creating a repository pattern. The code I have made is in a class library which you can clone from here:
 
 git clone https://github.com/toreaurstadboss/AzureCosmosDbRepositoryLib.git
 

To get started with Azure Cosmos DB, you must create a user first that is against Azure Cosmos DB. This will be your 'db user' in the cloud of course. When you start up Azure Cosmos DB, select the data explorer tab to view your data, where you can enter manual queries and look at data (and manipulate it). Note that there are already more official packages for repository pattern .NET SDK available by David Pine, which you should consider using as shown here: https://docs.microsoft.com/en-us/events/azure-cosmos-db-azure-cosmos-db-conf/a-deep-dive-into-the-cosmos-db-repository-pattern-dotnet-sdk However, I have also published and pushed a simple GitHub repo I have created here which might be easier to get started with and understand. My goal anyways was to have a learning experience myself with testing out Azure Cosmos DB. The Github repo is available here: https://github.com/toreaurstadboss/AzureCosmosDbRepositoryLib The methods of the repository is listed inside IRepository :
 
  using AzureCosmosDbRepositoryLib.Contracts;
using Microsoft.Azure.Cosmos;
using System.Linq.Expressions;

namespace AzureCosmosDbRepositoryLib;


/// <summary>
/// Repository pattern for Azure Cosmos DB
/// </summary>
public interface IRepository<T> where T : IStorableEntity
{

    /// <summary>
    /// Adds an item to container in DB. 
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="item"></param>
    /// <returns></returns>
    Task<ISingleResult<T>?> Add(T item);

    /// <summary>
    /// Retrieves an item to container in DB. Param <paramref name="partitionKey"/> and param <paramref name="id"/> should be provided.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="id"></param>
    /// <returns></returns>
    Task<ISingleResult<T>?> Get(IdWithPartitionKey id);

    /// <summary>
    /// Searches for a matching items by predicate (where condition) given in <paramref name="searchRequest"/>.
    /// </summary>
    /// <param name="searchRequest"></param>
    /// <returns></returns>
    Task<ICollectionResult<T>?> Find(ISearchRequest<T> searchRequest);

    /// <summary>
    /// Searches for a matching items by predicate (where condition) given in <paramref name="searchRequest"/>.
    /// </summary>
    /// <param name="searchRequest"></param>
    /// <returns></returns>
    Task<ISingleResult<T>?> FindOne(ISearchRequest<T> searchRequest);

    /// <summary>
    /// Removes an item from container in DB. Param <paramref name="partitionKey"/> and param <paramref name="id"/> should be provided.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="partitionKey"></param>
    /// <param name="id"></param>
    /// <returns></returns>
    Task<ISingleResult<T>?> Remove(IdWithPartitionKey id);

    /// <summary>
    /// Removes items from container in DB. Param <paramref name="ids"/> must be provided.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="partitionKey"></param>
    /// <param name="id"></param>
    /// <returns></returns>
    Task<ICollectionResult<T>?> RemoveRange(List<IdWithPartitionKey> ids);

    /// <summary>
    /// Adds a set of items to container in DB. A shared partitionkey is used and the items are added inside a transaction as a single operation.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="items"></param>
    /// <param name="partitionKey"></param>
    /// <returns></returns>
    Task<ICollectionResult<T>?> AddRange(IDictionary<PartitionKey, T> items);

    /// <summary>
    /// Adds or updates items via 'Upsert' method in container in DB. 
    /// </summary>
    /// <param name="item"></param>
    /// <returns></returns>
    Task<ICollectionResult<T>?> AddOrUpdateRange(IDictionary<PartitionKey, T> items);


    /// <summary>
    /// Adds or updates an item via 'Upsert' method in container in DB. 
    /// </summary>
    /// <param name="item"></param>
    /// <returns></returns>
    Task<ISingleResult<T>?> AddOrUpdate(T item);

    /// <summary>
    /// Retrieves results paginated of page size. Looks at all items of type <typeparamref name="T"/> in the container. Send in a null value for continuationToken in first request and then use subsequent returned continuation tokens to 'sweep through' the paged data divided by <paramref name="pageSize"/>.
    /// </summary>
    /// <param name="pageSize"></param>
    /// <param name="continuationToken"></param>
    /// <param name="sortDescending">If true, sorting descending (sorting via LastUpdate property so newest items shows first)</param>
    /// <returns></returns>
    Task<IPaginatedResult<T>?> GetAllPaginated(int pageSize, string? continuationToken = null, bool sortDescending = false, Expression<Func<T, object>>[]? sortByMembers = null);

    /// <summary>
    /// On demand method exposed from exposing this respository on demands. Frees up resources such as CosmosClient object inside.
    /// </summary>
    void Dispose();

    /// <summary>
    /// Returns name of database in Azure Cosmos DB
    /// </summary>
    /// <returns></returns>
    string? GetDatabaseName();

    /// <summary>
    /// Returns Container id inside database in Azure Cosmos DB
    /// </summary>
    /// <returns></returns>
    string? GetContainerId(); 

}
  
 
 
Let's first look at retrieving the paginated results using a 'continuation token' which is how you do paging inside Azure Cosmos DB where this token is a 'bookmark'.
 
 
  public async Task<IPaginatedResult<T>?> GetAllPaginated(int pageSize, string? continuationToken = null, bool sortDescending = false,
        Expression<Func<T, object>>[]? sortByMembers = null)
    {
        string sortByMemberNames = sortByMembers == null ? "c.LastUpdate" :
            string.Join(",", sortByMembers.Select(x => "c." + x.GetMemberName()).ToArray()); 
        var query = new QueryDefinition($"SELECT * FROM c ORDER BY {sortByMemberNames} {(sortDescending ? "DESC" : "ASC")}".Trim()); //default query - will filter to type T via 'ItemQueryIterator<T>' 
        var queryRequestOptions = new QueryRequestOptions
        {
            MaxItemCount = pageSize
        };
        var queryResultSetIterator = _container.GetItemQueryIterator<T>(query, requestOptions: queryRequestOptions,
            continuationToken: continuationToken);
        var result = queryResultSetIterator.HasMoreResults ? await queryResultSetIterator.ReadNextAsync() : null;
        if (result == null)
            return null!;

        var sourceContinuationToken = result.ContinuationToken;
        var paginatedResult = new PaginatedResult<T>(sourceContinuationToken, result.Resource);
        return paginatedResult;

    }
 
 
We sort by LastUpdate member default and we send in the pagesize, sorting ascending default and allowing to specify sorting members. A helper method to get the name of the property expressions to use as sorting member is also used here. We get a query item iterator from the 'container' and then read the found items which is in the Resource property, all asynchronously. Note that we return the continutation token here in the end and we initially send in null as the continuation token. Each call to getting a new page will get a new continuation token so we can browse through the data in pages. When the continuation token is null, we have come to the end of the data. PaginatedResult looks like this:
 
 
namespace AzureCosmosDbRepositoryLib.Contracts
{

    public interface IPaginatedResult<T>
    {
        public IList<T> Items { get; }
        public string? ContinuationToken { get; set; }
    }

    public class PaginatedResult<T> : IPaginatedResult<T>
    {
        public PaginatedResult(string continuationToken, IEnumerable<T> items)
        {
            Items = new List<T>();
            if (items != null)
            {
                foreach (var item in items)
                {
                    Items.Add(item);
                }
            }
            ContinuationToken = continuationToken;
        }
        public IList<T> Items { get; private set; }
        public string? ContinuationToken { get; set; }
    }
}

 
Another thing in the lib is the contract IStorableEntity, which is a generic interface of type T, which defined a Id property - note the usage of JsonProperty attribute. Also, we set up a partition key for the item here.
 
 
using Microsoft.Azure.Cosmos;
using Newtonsoft.Json;

namespace AzureCosmosDbRepositoryLib.Contracts
{
    public interface IStorableEntity
    {
        [JsonProperty("id")]
        string Id { get; set; }

        PartitionKey? PartitionKey { get; }

        DateTime? LastUpdate { get; set; }
    }
} 
 
 
It is important to both have set the id and partitionkey when you save, update and delete items in container in Azure Cosmos DB so it works as expected. There are other methods in this repo as seen in the IRepository interface. The repository class will take care of creating the database and container in Azure Cosmos DB if required. Note also that it is important in intranet scenarios to set up Gateway connection mode. This is done default and the reason why this is done is because of firewall issues.
 
 
  private void InitializeDatabaseAndContainer(CosmosClientOptions? clientOptions, ThroughputProperties? throughputPropertiesForDatabase, bool defaultToUsingGateway)
    {
        _client = clientOptions == null ?
            defaultToUsingGateway ?
            new CosmosClient(_connectionString, new CosmosClientOptions
            {
                ConnectionMode = ConnectionMode.Gateway //this is the connection mode that works best in intranet-environments and should be considered as best compatible approach to avoid firewall issues
            }) :
            new CosmosClient(_connectionString) :
            new CosmosClient(_connectionString, _cosmosClientOptions);

        //Run initialization 
        if (throughputPropertiesForDatabase == null)
        {
            _database = Task.Run(async () => await _client.CreateDatabaseIfNotExistsAsync(_databaseName)).Result; //create the database if not existing (will go for default options regarding scaling)
        }
        else
        {
            _database = Task.Run(async () => await _client.CreateDatabaseIfNotExistsAsync(_databaseName, throughputPropertiesForDatabase)).Result; //create the database if not existing - specify specific through put options
        }

        // The container we will create.  
        _container = Task.Run(async () => await _database.CreateContainerIfNotExistsAsync(_containerId, _partitionKeyPath)).Result;
    } 
 
 
Another example using another iterator than the item query generator is the linq query generator. This is used inside the Find method :
 
 public async Task<ICollectionResult<T>?> Find(ISearchRequest<T>? searchRequest)
    {
        if (searchRequest?.Filter == null)
            return await Task.FromResult<ICollectionResult<T>?>(null);
        var linqQueryable = _container.GetItemLinqQueryable<T>();
        var stopWatch = Stopwatch.StartNew();
        try
        {
            using var feedIterator = linqQueryable.Where(searchRequest.Filter).ToFeedIterator();
            while (feedIterator.HasMoreResults)
            {
                var items = await feedIterator.ReadNextAsync();
                var result = BuildSearchResultCollection(items.Resource);
                result.ExecutionTimeInMs = stopWatch.ElapsedMilliseconds;
                return result;
            }
        }
        catch (Exception err)
        {
            return await Task.FromResult(BuildSearchResultCollection(err));
        }
        return await Task.FromResult<ICollectionResult<T>?>(null);
    } 
 
 
 
 
using System.Linq.Expressions;
namespace AzureCosmosDbRepositoryLib.Contracts
{
    public interface ISearchRequest<T>
    {
        Expression<Func<T, bool>>? Filter { get; set; }
    }

    public class SearchRequest<T> : ISearchRequest<T>
    {
        public Expression<Func<T, bool>>? Filter { get; set; }
    }
} 
 
Note that this lib is using Microsoft.Azure.Cosmos of version 3.29. There are differences between the major version obviously, so the methods shown here applies to Azure Cosmos DB 3.x. This is the only nuget package this lib requires. You should consider the SDK that David Pine created, but if you want to create a repository pattern against Azure Cosmos DB - then maybe you find my repository pattern a starting point and you can borrow some code from it. One final note - I had troubles doing a batch insert in the lib for a type of T using transactions in Azure Cosmos DB, that this seems to require a common partition key - it ended up in a colission. So the AddRange method in the lib is not batched with one partition but done sequentially looping through the items for now.. Other than that - the lib should work for some core usages in ordinary scenarios. The lib should log errors a bit better too, so the lib is primarily for demonstration usages and showing essential CRUD operations in Azure Cosmos DB. Note that the connection string should be saved into a appsettings.json file for example or in dev environments consider using dotnet user secrets as I have done so we do not expose secrets to source control. The connection string is shown under the 'Keys' tab in the Azure cosmos. You will look for 'primary connetion string' here as this is how you connect to your database and container(s), where data resides. Use the 'data explorer' tab to work with the data.

Saturday, 16 July 2022

Using ValueConverter in EF Core 6 to handle custom data types

Usually we stick to the familiar data types, but EF Core 6 (Entity Framework) can also both save a custom data type and also load it up again. Usually, you would save the data type in a familiar representation and then have some mapper method map into the desired type in a data transfer object - DTO. In this example we will anyways look at an alternative, a ValueConverter. This have been supported since EF Core 2.1. We first off define a ValueConverter by inhering from the ValueConverter class. This takes a 'from' and 'to' generic type argument. We convert from a type into another type and vice versa. We send in two expressions to do these mappings to the base constructor. I have pushed the code here if you want to test out ValueConverter in EF Core yourself.
git clone https://github.com/toreaurstadboss/EfCore6ValueConversionTemplate.git Let's see how we for example can save a Color (System.Drawing.Color) via its color name and load it up again.
 
 
 using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
 using System.Drawing;
 using System.Linq.Expressions;

 namespace PublisherData
 {
 
    public class ColorValueConverter : ValueConverter<Color, string>
    {
        private static Expression<Func<Color, string>> ColorString =
            c => new string(c.Name);
        static Expression<Func<string, Color>> ColorStruct =
            s => Color.FromName(s);

        public ColorValueConverter() : base(ColorString, ColorStruct) { 
        }
    }

 } 
 
Next up we 'bulk-configure' a value converter for the Color type to have conversion set to this ValueConverter. Inside the db context class we do :
 
 
  protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
  {
  	configurationBuilder.Properties<Color>().HaveConversion(typeof(ColorValueConverter)); 
  }
 
 
We also define a migration for database and add a column in a table to save the color, which is defined as a string (varchar(100)) column, so we can test out the conversion.
 
  using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace PublisherData.Migrations
{
    public partial class favcolor : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.AddColumn<string>(
                name: "FavoriteColor",
                table: "Authors",
                type: "nvarchar(100)",
                maxLength: 100,
                nullable: true);
        }

        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropColumn(
                name: "FavoriteColor",
                table: "Authors");
        }
    }
}

 
A small console app then tests out the Value conversion :
 
 
 // See https://aka.ms/new-console-template for more information
 using Microsoft.EntityFrameworkCore;
 using PublisherData;
 using System.Drawing;

 Console.WriteLine("Posting a new author - testing out ValueConverter feature of EF Core 6");


 var options = new DbContextOptionsBuilder<PubContext>()
    .UseSqlServer("Data Source = somedevmachine\\sqlexpress01; Initial Catalog = PubDatabase_M12_Api_EfCoreCourse; Trusted_Connection=true");


 using var pubContext = new PubContext(options.Options);

 var authorInsert = new PublisherDomain.Author
  {
    FirstName = "Claude",
    LastName = "Monet",
    FavoriteColor = Color.Aquamarine
  }; 

 pubContext.Authors.Add(authorInsert);
 await pubContext.SaveChangesAsync();
 var insertedAuthor = pubContext.Authors.Find(authorInsert.AuthorId);
 string convertColorToArgbName(Color someColor) => Color.FromArgb(someColor.ToArgb()).Name;
 string insertedAuthorFavoriteColorArgb = convertColorToArgbName(authorInsert.FavoriteColor.Value);
 string aquaMarineColorArgb = convertColorToArgbName(Color.Aquamarine);

 Console.WriteLine($"Favorite color is expected? {insertedAuthorFavoriteColorArgb.Equals(aquaMarineColorArgb)}");

 
We save the favorite color and when we load up the entity again we load the expected color by checking the Argb value 'Name' property (the hexadecimal color value acutally). The output then reads:
Posting a new author - testing out ValueConverter feature of EF Core 6 Favorite color is expected? True This is a rather trivial example. We could have a more complex object which we wanted to save in a particular way to the database and load it up again. For some reason or another, that object might not just be serialized, saved and restored and loaded via deserialization. Anyways, ValueConverter is a feature which I wanted to test out in EF Core and it is an alternative to be at least aware of.

Saturday, 9 July 2022

MediatR - Adding a pipeline behavior and logging to SeriLog and Seq

This article will present how you can add a pipeline behavior to MediatR and then log to SeriLog and Seq. This makes it possible to add for example logging and use the 'decorator pattern' with MediatR to add behavior to the pipeline of Asp.net core in general (.net 6 is used here). It is therefore sort of Middelware and tied together via MediatR. First off, lets define the IPipelineBehavior, which will do our logging. Note that you have to add the generic type parameter where condition here of TRequest. Note that we inject the ILogger<LoggingBehavior<TRequest, TResponse>> logger object here. In .net (core) we send in ILogger instance of the type of the class we are injecting into to log out to log sources and SeriLog will present this information. Inside Seq we can also browse this log information. We only log payloads if we are inside Development environment and the payload of request / response can be serialized and is below 50 kB.
 
 Program.cs
 
using MediatR;
using Newtonsoft.Json;
using System.Diagnostics;

namespace CqrsDemoWebApi.Features.Books.Pipeline
{

    public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse>
    {
        private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;
        private readonly IWebHostEnvironment _environment;

        public LoggingBehavior(ILogger<LoggingBehavior<TRequest, TResponse>> logger, IWebHostEnvironment environment)
        {
            _logger = logger;
            _environment = environment;
        }

        public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
        {
            string? requestData = null;

            if (_environment.IsDevelopment())
            {
                //log serializable payload data in request (as json format) to SeriLog if we are inside Development environment and the payload is below 50 kB in serializable size
                try
                {
                    requestData = request != null ? JsonConvert.SerializeObject(request) : null;
                    if (requestData?.Length > 50 * 1028)
                        requestData = null; //avoid showing data larger than 100 kB in serialized form in the logs of Seq / SeriLog                    
                }
                catch (Exception)
                {
                    //ignore 
                }
            }
            _logger.LogInformation($"Handling request {typeof(TRequest).Name} {{@requestData}}", requestData);
        
            var response = await next();

            string? responseData = null;
            if (_environment.IsDevelopment())
            {              
                //log serializable payload data in response (as json format) to SeriLog if we are in Development environment and the payload is below 50 kB in serialized size
                try
                {
                    responseData = response != null ? JsonConvert.SerializeObject(response) : null;
                    if (responseData?.Length > 50 * 1028)
                        responseData = null; //avoid showing data larger than 100 kB in serialized form in the logs of Seq / SeriLog
                }
                catch (Exception)
                {
                    //ignore 
                }
            }
            _logger.LogInformation($"Handled request {typeof(TRequest).Name}. Returning result of type {typeof(TResponse).Name} {{@responseData}}.", responseData);
            return response;
        }

    }

}
 
 
Afterwards register the IPipelineBehavior. Note how we also add UseSerilog method here and add Seq too.
 
 Program.cs
 
using CqrsDemoWebApi.Database;
using CqrsDemoWebApi.Features.Books.Pipeline;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Serilog;
using System.Reflection;

var builder = WebApplication.CreateBuilder(args);
builder.Host.UseSerilog((ctx, lc) =>
 lc.WriteTo.Console()
 .WriteTo.Seq("http://localhost:5341")); //note - to use Seq with this url - install it from here (free individual license) : https://datalust.co/download

// Add services to the container.

builder.Services.AddMediatR(Assembly.GetExecutingAssembly()); //adding MediatR support here. 
//register some pipeline(s) defined for MediatR usage
builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>)); 

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var connectionString = new ConnectionString(builder.Configuration.GetConnectionString("CqrsDemoBooksDb")); 
builder.Services.AddSingleton(connectionString);

builder.Services.AddDbContext<BooksDbContext>(options =>
options.UseSqlServer(connectionString.Value)); 

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
 
 
Here are the updated nuget packages inside the csproj file of the Asp.net core project (.net 6) :
 
 
  <ItemGroup>
    <PackageReference Include="Mediatr" Version="10.0.1" />
    <PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="10.0.1" />
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.1" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.1" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.1">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
    <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
    <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
    <PackageReference Include="Serilog.AspNetCore" Version="6.0.0-dev-00265" />
    <PackageReference Include="Serilog.Sinks.Seq" Version="5.1.2-dev-00225" />
    <PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
  </ItemGroup>
 
 
We also need to install Seq on our computer to see the messages sent to SeriLog inside Seq. Go to this url to download free individual license of Seq : https://datalust.co/download You can see the logs at Seq's default url and also set up in Program.cs file : http://localhost:5341