foreach (var iteration in Enumerable.Range(0, 20)){
var tæmpen = new Random().Next(-60, 60);
var været = $"Været e i dag {tæmpen}C og på Trondheimsdialækt: {tæmpen switch
{
<= -50 => "Du træng eitt nytt termometer. For kaldt!",
<= -35 => "Småfuggel'n dætt dau fra trær'n",
<= -30 => "Båinnspeika",
<= -25 => "Få inn katta!",
<= -20 => "Gnallerfrost",
<= -10 => "Kjøle kaaalt",
<= -5 => "Kaillvoli",
<= 5 => "Kaillhustri",
<= 10 => "Julivær",
<= 15 => "Godværsle",
<= 20 => "Kjøle varmt",
<= 25 => "Råådeili",
<= 30 => "Steikvarmt",
<= 40 => "Søkke heitt",
<= 50 => "Kokheitt",
_ => "Du træng eitt nytt termometer. For varmt!"
}}";
System.Console.WriteLine(været);
}
As we can see, in C# 11, we can do a lot more inside string interpolation expressions and now allow multiple lines, including relational patterns. It can be quite handy to use, when .NET 7 and C# 11 reaches mainstream usage.
The text here is based upon a dialect based thermometer, available for purchase from here:
https://dialekttempen.no/butikk/termometer/fylker/sor-trondelag/trondheim-munkholmen/#&gid=1&pid=1
Saturday, 4 March 2023
Trøndersk dialekttemp with C# 11 og and patterns
This article demonstrates the use of relational patterns in C# 11.
First off, relational patterns allow us to test how a given value /variable compares to constants. If we want to have multiple conditions we can use and operator not shown here.
I have here different conditions / intervals for temperaturs outputting the temperature and description of the weather in some local language of mine from Norway (Trøndersk / Trondheim city).
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
Powerpoint presentation (Norwegian, 11th december 2022) : https://1drv.ms/u/s!AhGGDxs-tzqJcFrls6Fue8Xnjx4?e=lWYYwU
Etiketter:
Apidesign,
Blazor,
csharp,
DotNetCore,
EF,
EFCore6,
EntityFrameworkCore,
GraphQL,
HotChocolate,
net6
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
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();
}
//..
Subscribe to:
Comments (Atom)



