├── .github ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── update-page.yml │ └── verify-pr.yml ├── .vscode └── tipsandtricks.code-snippets ├── README.md ├── docs ├── advanced │ ├── SIMD.md │ ├── arraypool.md │ ├── boxing.md │ ├── index.md │ ├── iterate_list.md │ ├── lambda_methodgroup.md │ ├── lazy.md │ ├── span.md │ ├── stackalloc.md │ └── struct.md ├── array.md ├── async_await.md ├── blazor.md ├── collections.md ├── debugging.md ├── dictionary.md ├── efcore │ ├── asnotracking.md │ ├── detailed_logging.md │ ├── in_memory_sqlite.md │ ├── index.md │ ├── loadasync.md │ ├── maxlength_strings.md │ ├── retry.md │ └── splitquery.md ├── exceptions.md ├── index.md ├── linq.md ├── logging │ ├── compiletime_logging.md │ ├── index.md │ └── string_interpolation.md ├── misc.md ├── nuget │ ├── floating_versioning.md │ └── index.md ├── regex │ ├── index.md │ ├── regex_options.md │ ├── right_to_left.md │ └── source_generator.md ├── strings.md └── valuetuple.md └── mkdocs.yml /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Pull request description 2 | 6 | 7 | ### PR meta checklist 8 | - [ ] Pull request is targeted at `main` branch for code 9 | 10 | ### Code PR specific checklist 11 | - [ ] If I added a new section, I added this to the `mkdocs.yml` 12 | - [ ] All links are valid -------------------------------------------------------------------------------- /.github/workflows/update-page.yml: -------------------------------------------------------------------------------- 1 | name: Update page 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | 9 | markdown-link-check: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@master 13 | - uses: gaurav-nelson/github-action-markdown-link-check@v1 14 | 15 | build: 16 | name: Deploy docs 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout main 20 | uses: actions/checkout@v4 21 | 22 | - name: Collect entries and sections 23 | run: | 24 | echo "entries=$(grep -ioR '^## ' * | wc -l)" >> $GITHUB_ENV 25 | 26 | - name: Replace Tokens in index.md 27 | uses: cschleiden/replace-tokens@master 28 | with: 29 | files: 'docs/index.md' 30 | env: 31 | entries: "${{ env.entries }}" 32 | 33 | - name: Deploy MkDocs 34 | uses: mhausenblas/mkdocs-deploy-gh-pages@master 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/verify-pr.yml: -------------------------------------------------------------------------------- 1 | name: Verify links 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | 9 | markdown-link-check: 10 | name: Verify links 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@master 14 | - uses: gaurav-nelson/github-action-markdown-link-check@v1 -------------------------------------------------------------------------------- /.vscode/tipsandtricks.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "New tip": { 3 | "prefix": "new", 4 | "body": "## ${1:title}\n${2:description}\n\n❌ **Bad** ${3:bad}\n```csharp\n${4:code}\n```\n\n✅ **Good** ${5:good}\n```csharp\n${6:code}\n```", 5 | "description": "Inserts the new template with the good and bad icon." 6 | } 7 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tips and tricks 2 | A collection of tips, tricks and smaller pitfalls with smaller code snippets and explanation. 3 | 4 | Right now there are **80+** entries in the collection. 5 | The page has multiple sections divided by topic like `async`/`await` or `Blazor` and should give a good overview over common scenarios. 6 | 7 | Go ahead to the official page here: [tips and tricks](https://linkdotnet.github.io/tips-and-tricks). 8 | 9 | 10 | # Support & Contributing 11 | It helps out if you give this repository a ⭐. 12 | 13 | Feel free to contribute samples via [Pull Request](https://github.com/linkdotnet/tips-and-tricks/pulls). 14 | 15 | Thanks to all [contributors](https://github.com/linkdotnet/tips-and-tricks/graphs/contributors): 16 | 17 | 18 | 19 | 20 | 21 | # Contact 22 | If you have any questions, let me know. You can reach me here: 23 | 24 | 25 | [![Linkedin Badge](https://img.shields.io/badge/Steven%20Giesel-0077B5?style=flat&logo=linkedin&logoColor=white)](https://www.linkedin.com/in/steven-giesel/) 26 | [![Github Badge](https://img.shields.io/badge/linkdotnet-100000?style=flate&logo=github&logoColor=white)](https://github.com/linkdotnet/) 27 | [![Blog Badge](https://img.shields.io/badge/Steven%20Giesel-FFA500?style=flat&logo=rss&logoColor=white)](https://steven-giesel.com/) -------------------------------------------------------------------------------- /docs/advanced/SIMD.md: -------------------------------------------------------------------------------- 1 | # SIMD - Single Input Multiple Data 2 | SIMD describes the ability the process a single operation simultaneously on multiple data. This can give a big edge on independent data which can be processed in parallel. More information can be found [here](https://steven-giesel.com/blogPost/d80d9367-3a1f-407f-9bdb-067fae9ea527). 3 | 4 | ## Compare two lists with SIMD 5 | To speed-up the process of two lists, we can vectorize those lists and compare multiple chunks in parallel. To vectorize a list we can use **SSE** (**S** IMD **S** treaming **E** xtensions) in combination with some helper tools from the .NET Framework itself. 6 | 7 | ❌ **Bad** Use LINQ queries, which can be slow. 8 | ```csharp 9 | var isEqual = list1.SequenceEquals(list2); 10 | ``` 11 | 12 | ✅ **Good** Use SSE to vectorize the list and check in parallel if the chunks are the same. 13 | ```csharp 14 | public bool SIMDContains() 15 | { 16 | // Lists are not equal when the count is different 17 | if (list1.Count != list2.Count) 18 | return false; 19 | 20 | // Create a Span> from our original list. 21 | // We don't have to worry about the internal size, etc... 22 | var list1AsVector = MemoryMarshal.Cast>(CollectionsMarshal.AsSpan(list1)); 23 | var list2AsVector = MemoryMarshal.Cast>(CollectionsMarshal.AsSpan(list2)); 24 | 25 | for (var i = 0; i < list1AsVector.Length; i++) 26 | { 27 | // Compare the chunks of list1 and list2 28 | if (!Vector.EqualsAll(list1AsVector[i], list2AsVector[i])) 29 | return false; 30 | } 31 | 32 | return true; 33 | } 34 | ``` 35 | 36 | > 💡 Info: Check with [`Vector.IsHardwareAccelerated`](https://docs.microsoft.com/en-us/dotnet/api/system.numerics.vector.ishardwareaccelerated?view=net-6.0) if SSE is available. If not the emulated / software version can be slower than LINQ due to the overhead. 37 | 38 | ### Benchmark 39 | ```csharp 40 | public class Benchmark 41 | { 42 | private readonly List list1 = Enumerable.Range(0, 1_000).ToList(); 43 | private readonly List list2 = Enumerable.Range(0, 1_000).ToList(); 44 | 45 | [Benchmark(Baseline = true)] 46 | public bool LINQSequenceEquals() => list1.SequenceEqual(list2); 47 | 48 | [Benchmark] 49 | public bool SIMDEquals() 50 | { 51 | if (list1.Count != list2.Count) 52 | return false; 53 | 54 | var list1AsVector = MemoryMarshal.Cast>(CollectionsMarshal.AsSpan(list1)); 55 | var list2AsVector = MemoryMarshal.Cast>(CollectionsMarshal.AsSpan(list2)); 56 | 57 | for (var i = 0; i < list1AsVector.Length; i++) 58 | { 59 | if (!Vector.EqualsAll(list1AsVector[i], list2AsVector[i])) 60 | return false; 61 | } 62 | 63 | return true; 64 | } 65 | } 66 | ``` 67 | 68 | Results: 69 | ```csharp 70 | | Method | Mean | Error | StdDev | Ratio | 71 | |------------------- |-----------:|---------:|---------:|------:| 72 | | LINQSequenceEquals | 3,487.4 ns | 13.72 ns | 10.71 ns | 1.00 | 73 | | SIMDEquals | 137.3 ns | 0.98 ns | 0.91 ns | 0.04 | 74 | ``` 75 | 76 | ## Get the sum of a list or array 77 | SIMD can be used to divide and conquer the problem of retrieving the sum of a list. The idea is to cut the list in smaller chunks and add them up via SIMD instuctions. This approach can faster the speed significantly. 78 | 79 | ❌ **Bad** Using plain old LINQ to get a sum of a list or array of integers. 80 | ```csharp 81 | _list.Sum(); 82 | ``` 83 | 84 | ✅ **Good** Using SIMD instructions to divide and conquer and parallelize the addition of the individual vectors. 85 | ```csharp 86 | public int SumSIMD() 87 | { 88 | var accVector = Vector.Zero; 89 | 90 | // For any array use 91 | // var spanOfVectors = MemoryMarshal.Cast>(new Span(myArray)); 92 | var spanOfVectors = MemoryMarshal.Cast>(CollectionsMarshal.AsSpan(_list)); 93 | foreach (var vector in spanOfVectors) 94 | { 95 | accVector = Vector.Add(accVector, vector); 96 | } 97 | 98 | // Scalar-Product of our vector against the Unit vector is its sum 99 | var result = Vector.Dot(accVector, Vector.One); 100 | return result; 101 | } 102 | ``` 103 | 104 | ### Benchmark 105 | ```csharp 106 | public class Benchmark 107 | { 108 | private readonly List _list = Enumerable.Range(0, 1_000).ToList(); 109 | 110 | [Benchmark(Baseline = true)] 111 | public int ListSum() => _list.Sum(); 112 | 113 | [Benchmark] 114 | public int SumSIMD() 115 | { 116 | var accVector = Vector.Zero; 117 | 118 | // For any array use 119 | // var spanOfVectors = MemoryMarshal.Cast>(new Span(myArray)); 120 | var spanOfVectors = MemoryMarshal.Cast>(CollectionsMarshal.AsSpan(_list)); 121 | foreach (var vector in spanOfVectors) 122 | { 123 | accVector = Vector.Add(accVector, vector); 124 | } 125 | 126 | // Scalar-Product of our vector against the Unit vector is its sum 127 | var result = Vector.Dot(accVector, Vector.One); 128 | return result; 129 | } 130 | } 131 | ``` 132 | Results: 133 | ```csharp 134 | | Method | Mean | Error | StdDev | Ratio | 135 | |-------- |-----------:|---------:|---------:|------:| 136 | | ListSum | 4,493.1 ns | 88.84 ns | 83.10 ns | 1.00 | 137 | | SumSIMD | 117.7 ns | 0.44 ns | 0.41 ns | 0.03 | 138 | ``` 139 | 140 | ## Getting Minimum and Maximum of a list 141 | SIMD can be used to get the smallest and the largest number in a given list. 142 | 143 | ```csharp 144 | public (int min, int max) GetMinMaxSIMD() 145 | { 146 | var vectors = MemoryMarshal.Cast>(CollectionsMarshal.AsSpan(_numbers)); 147 | var vMin = new Vector(int.MaxValue); 148 | var vMax = new Vector(int.MinValue); 149 | foreach (var vector in vectors) 150 | { 151 | vMin = Vector.Min(vMin, vector); 152 | vMax = Vector.Max(vMax, vector); 153 | } 154 | 155 | var min = int.MaxValue; 156 | var max = int.MinValue; 157 | 158 | for (var i = 0; i < Vector.Count; i++) 159 | { 160 | min = Math.Min(vMin[i], min); 161 | max = Math.Max(vMax[i], min); 162 | } 163 | 164 | return (min, max); 165 | } 166 | ``` 167 | 168 | The traditional versionl, could look like this: 169 | ```csharp 170 | public (int min, int max) GetMinMax() 171 | { 172 | var min = int.MaxValue; 173 | var max = int.MinValue; 174 | 175 | for (var i = 0; i < _numbers.Count; i++) 176 | { 177 | min = Math.Min(_numbers[i], min); 178 | max = Math.Max(_numbers[i], min); 179 | } 180 | 181 | return (min, max); 182 | } 183 | ``` 184 | 185 | Results: 186 | 187 | The given Llist (`_numbers`) has 10000 entries. 188 | 189 | ```csharp 190 | | Method | Mean | Error | StdDev | Ratio | Rank | 191 | |-------------- |----------:|----------:|----------:|------:|-----:| 192 | | GetMinMaxSIMD | 1.544 us | 0.0050 us | 0.0047 us | 0.10 | 1 | 193 | | GetMinMax | 15.763 us | 0.0435 us | 0.0407 us | 1.00 | 2 | 194 | ``` -------------------------------------------------------------------------------- /docs/advanced/arraypool.md: -------------------------------------------------------------------------------- 1 | # ArrayPool 2 | An `ArrayPool` is a reusable memory pool which tries to decrease garbage collection and memory pressure and therefore enables better performance. Using the array pool normally consists out of three parts: 3 | 4 | * Renting some array from the `ArrayPool`. 5 | * Doing something with the array. 6 | * Returning the rented array back to the `ArrayPool`. 7 | 8 | As seen by the pattern it is most useful when a lot of smaller arrays are needed in a short time. The major downside of an `ArrayPool` is that the consumer is now responsible to give back the memory instead of the GC. 9 | 10 | ## Using the shared ArrayPool 11 | `ArrayPool.Shared` is a predefined array pool that can directly be used. 12 | 13 | ```csharp 14 | var array = ArrayPool.Shared.Rent(1024); 15 | // Do something with the array here 16 | ArrayPool.Shared.Return(array); 17 | ``` 18 | 19 | ## Not returning the rented array 20 | ❌ **Bad** Don't returning the rented array can lead to memory depletion and performance hits as the pool has to be regenerated. The following example uses the `ArrayPool` in the constructor but does not give it back afterward. 21 | ```csharp 22 | public class MyArrayType 23 | { 24 | private int[] _array; 25 | 26 | public MyArrayType() 27 | { 28 | _array = ArrayPool.Shared.Rent(1024); 29 | } 30 | } 31 | ``` 32 | 33 | ✅ **Good** Return the array list in `Dispose` to guarantee the `ArrayPool` will not be depleted. 34 | ```csharp 35 | public class MyArrayType : IDisposable 36 | { 37 | private int[] _array; 38 | 39 | public MyArrayType() 40 | { 41 | _array = ArrayPool.Shared.Rent(1024); 42 | } 43 | 44 | public void Dispose() 45 | { 46 | if (_array != null) 47 | { 48 | ArrayPool.Shared.Return(_array); 49 | } 50 | } 51 | } 52 | ``` 53 | 54 | ## Assuming you get the exact length you are asking for 55 | 56 | ❌ **Bad** Assuming that you get the exact amount you asked for. [`Rent`](https://docs.microsoft.com/en-us/dotnet/api/system.buffers.arraypool-1.rent?view=net-6.0) only parameter is called `minimumLength`. That means you can get a bigger array than you are asking for. 57 | 58 | ```csharp 59 | var array = ArrayPool.Shared.Rent(1024); 60 | for (var i = 0; i < array.Length; i++) // can be more than 1024 iterations 61 | { 62 | // ... 63 | } 64 | 65 | ArrayPool.Shared.Return(array); 66 | ``` 67 | 68 | ✅ **Good** Use of a const variable. 69 | ```csharp 70 | const int size = 1024; 71 | var array = ArrayPool.Shared.Rent(size); 72 | for (var i = 0; i < size; i++) 73 | { 74 | // ... 75 | } 76 | 77 | ArrayPool.Shared.Return(array); 78 | ``` 79 | 80 | ## Assuming it is null initialized 81 | ❌ **Bad** Renting array from the array is not necessarily zero-initialized. 82 | ```csharp 83 | var array = ArrayPool.Shared.Rent(3); 84 | var zero = array[0] + array[1] + array[2]; // Is not necessary 0 85 | ``` 86 | 87 | The following example shows that the program rents an array from the pool and sets some values. Afterward, it returns this memory to the pool. 88 | Once more retrieving these values, the program can read its old values. 89 | ```csharp 90 | for(var i = 0; i < 4;i++) 91 | { 92 | var array = ArrayPool.Shared.Rent(4); 93 | array.SetValue(1, i); 94 | ArrayPool.Shared.Return(array); 95 | } 96 | 97 | var rentedArray = ArrayPool.Shared.Rent(8); 98 | for (var i = 0; i < 4; i++) 99 | { 100 | Console.WriteLine(rentedArray[i]); 101 | } 102 | ``` 103 | 104 | Output: 105 | ``` 106 | 1 107 | 1 108 | 1 109 | 1 110 | ``` 111 | 112 | ## Performance 113 | 114 | ```csharp 115 | [MemoryDiagnoser] 116 | public class Benchmark 117 | { 118 | const int NumberOfItems = 10000; 119 | 120 | [Benchmark] 121 | public void ArrayViaNew() 122 | { 123 | var array = new int[NumberOfItems]; 124 | } 125 | [Benchmark] 126 | public void SharedArrayPool() 127 | { 128 | var array = ArrayPool.Shared.Rent(NumberOfItems); 129 | ArrayPool.Shared.Return(array); 130 | } 131 | } 132 | ``` 133 | 134 | Results: 135 | ``` 136 | | Method | Mean | Error | StdDev | Gen 0 | Allocated | 137 | |---------------- |------------:|-----------:|-----------:|-------:|----------:| 138 | | ArrayViaNew | 3,387.04 ns | 263.598 ns | 773.088 ns | 9.5215 | 40,024 B | 139 | | SharedArrayPool | 40.60 ns | 2.531 ns | 7.462 ns | - | - | 140 | ``` -------------------------------------------------------------------------------- /docs/advanced/boxing.md: -------------------------------------------------------------------------------- 1 | # Boxing / Unboxing 2 | This chapter shows (hidden) pitfalls in terms of boxing and unboxing. 3 | 4 | ## Enumerate through `IList` vs `List` 5 | When you call `List.GetEnumerator()` (which will be done in every foreach loop) you get a struct named `Enumerator`. When calling `IList.GetEnumerator()` you get a variable of type `IEnumerator`, which contains a boxed version of your value type. In the performance critical section, this can be important. 6 | 7 | ❌ **Bad** This will box every individual integer in the list when enumerated through. 8 | ```csharp 9 | private int GetSum(IList numbers) 10 | { 11 | foreach (var number in numbers) 12 | // ... 13 | } 14 | ``` 15 | 16 | ✅ **Good** Using the `List` implementation avoids the boxing. 17 | ```csharp 18 | private int GetSum(List numbers) 19 | { 20 | foreach (var number in numbers) 21 | // ... 22 | } 23 | ``` 24 | 25 | > 💡 Info: The same applies to `Collection` and `ICollection`. Basically everytime when you use the interface, changes are that the values get boxed. 26 | > This also happens with LINQ queries. 27 | 28 | ### Benchmark 29 | ```csharp 30 | public class Benchmark 31 | { 32 | private readonly List numbersList = Enumerable.Range(0, 10_000).ToList(); 33 | private readonly IList numbersIList = Enumerable.Range(0, 10_000).ToList(); 34 | 35 | [Benchmark(Baseline = true)] 36 | public int GetSumOfList() 37 | { 38 | var sum = 0; 39 | foreach (var number in numbersList) { sum += number; } 40 | return sum; 41 | } 42 | 43 | [Benchmark] 44 | public int GetSumOfIList() 45 | { 46 | var sum = 0; 47 | foreach (var number in numbersIList) { sum += number; } 48 | return sum; 49 | } 50 | } 51 | ``` 52 | 53 | Results: 54 | ```csharp 55 | | Method | Mean | Error | StdDev | Ratio | RatioSD | Allocated | Alloc Ratio | 56 | |-------------- |----------:|----------:|----------:|------:|--------:|----------:|------------:| 57 | | GetSumOfList | 9.392 us | 0.0186 us | 0.0165 us | 1.00 | 0.00 | - | NA | 58 | | GetSumOfIList | 44.098 us | 0.3359 us | 0.3142 us | 4.69 | 0.03 | 40 B | NA | 59 | ``` 60 | 61 | ## Using `List` in LINQ queries will box the enumerator 62 | As LINQ queries are built upon `IEnumerable` passing a list of value types to a LINQ query will box the enumerator. This is especially unwanted in high performance path in your application. 63 | 64 | ❌ **Bad** Using LINQ query to get the sum of a `List`. 65 | ```csharp 66 | List numbers = GetNumbers(); 67 | 68 | var sum = numbers.Sum(); // This will box the enumerator 69 | ``` 70 | 71 | ✅ **Good** Use foreach with the enumerator of the list to avoid boxing. 72 | ```csharp 73 | List numbers = GetNumbers(); 74 | 75 | var sum = 0; 76 | foreach (var number in numbers) 77 | sum += number; 78 | ``` 79 | 80 | ### Benchmark 81 | ```csharp 82 | public class Benchmark 83 | { 84 | private readonly List numbersList = Enumerable.Range(0, 10_000).ToList(); 85 | 86 | [Benchmark(Baseline = true)] 87 | public int GetSumViaForeachList() 88 | { 89 | var sum = 0; 90 | foreach (var number in numbersList) 91 | sum += number; 92 | 93 | return sum; 94 | } 95 | 96 | [Benchmark] 97 | public int GetSumViaLINQ() => numbersList.Sum(); 98 | } 99 | 100 | ``` 101 | 102 | Results: 103 | ```csharp 104 | | Method | Mean | Error | StdDev | Ratio | RatioSD | Allocated | Alloc Ratio | 105 | |--------------------- |----------:|----------:|----------:|------:|--------:|----------:|------------:| 106 | | GetSumViaForeachList | 9.391 us | 0.0210 us | 0.0176 us | 1.00 | 0.00 | - | NA | 107 | | GetSumViaLINQ | 43.778 us | 0.1257 us | 0.1176 us | 4.66 | 0.02 | 40 B | NA | 108 | ``` -------------------------------------------------------------------------------- /docs/advanced/index.md: -------------------------------------------------------------------------------- 1 | # Advanced 2 | Here you will find more advanced techniques which are not meant for everyday use. This could be for example very minor optimization for hot paths which try to reduce every millisecond and every byte of allocation. 3 | The tips will most likely increase complexity and decrease readability in the source code. 4 | 5 | As always measure the specific scenario first and act according to the results. -------------------------------------------------------------------------------- /docs/advanced/iterate_list.md: -------------------------------------------------------------------------------- 1 | # Iterate through a list 2 | 3 | The page shows three different ways iterating through a List: `for`, `foreach` and via `Span`. 4 | 5 | Not all of them are the same in terms of behavior. 6 | 7 | `foreach` is well known and shows the intend very well. It is slower than a `for` loop mainly because foreach has to check if the collection gets modified. A `for` loop can suffer from "one of" issues where you access an index out of bounds. How often does that happen with a `foreach`? So even though `foreach` is slower, it is the safest and most verbose option (when you don't need the index). 8 | 9 | With [`CollectionMarshal.AsSpan`](https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.collectionsmarshal.asspan?view=net-6.0)since .NET 5 we can grab the internal array of List and use it as Span. This is the fastest thanks to a lot of optimizations. Like the `for` loop there are no checks if the original collection gets modified. You can also use the Span in a `foreach` struct and still the same holds true. 10 | Be aware as the unsafe nature of that function things can get messy. For example the `Span` can contain stale data, which in the original list is already removed. 11 | 12 | ## Performance comparison 13 | 14 | ```csharp 15 | public class Benchmark 16 | { 17 | [Params(10, 100, 1000)] 18 | public int ArraySize { get; set; } 19 | 20 | private List numbers; 21 | 22 | [GlobalSetup] 23 | public void Setup() 24 | { 25 | numbers = Enumerable.Repeat(0, ArraySize).ToList(); 26 | } 27 | 28 | [Benchmark(Baseline = true)] 29 | public void ForEach() 30 | { 31 | foreach (var num in numbers) 32 | { 33 | } 34 | } 35 | 36 | [Benchmark] 37 | public void For() 38 | { 39 | for (var i = 0; i < ArraySize; i++) 40 | { 41 | _ = numbers[i]; 42 | } 43 | } 44 | 45 | [Benchmark] 46 | public void Span() 47 | { 48 | var collectionAsSpan = CollectionsMarshal.AsSpan(numbers); 49 | for (var i = 0; i < ArraySize; i++) 50 | { 51 | _ = collectionAsSpan[i]; 52 | } 53 | } 54 | } 55 | ``` 56 | 57 | Results: 58 | ``` 59 | | Method | ArraySize | Mean | Error | StdDev | Ratio | RatioSD | 60 | |-------- |---------- |-------------:|-----------:|-----------:|------:|--------:| 61 | | ForEach | 10 | 14.558 ns | 0.3226 ns | 0.8328 ns | 1.00 | 0.00 | 62 | | For | 10 | 6.053 ns | 0.1391 ns | 0.2166 ns | 0.41 | 0.03 | 63 | | Span | 10 | 3.906 ns | 0.0988 ns | 0.0924 ns | 0.27 | 0.01 | 64 | | | | | | | | | 65 | | ForEach | 100 | 161.745 ns | 2.8664 ns | 3.4123 ns | 1.00 | 0.00 | 66 | | For | 100 | 67.268 ns | 1.1961 ns | 1.1188 ns | 0.41 | 0.01 | 67 | | Span | 100 | 50.935 ns | 1.0039 ns | 1.1158 ns | 0.31 | 0.01 | 68 | | | | | | | | | 69 | | ForEach | 1000 | 1,508.880 ns | 29.7007 ns | 36.4751 ns | 1.00 | 0.00 | 70 | | For | 1000 | 636.547 ns | 12.6593 ns | 25.8595 ns | 0.42 | 0.02 | 71 | | Span | 1000 | 337.738 ns | 6.7882 ns | 16.6517 ns | 0.22 | 0.01 | 72 | 73 | ``` 74 | ## Stale data 75 | Be aware that getting the memory slice of a `List` and modifying the `List` afterwards will **not** update the `Span`. 76 | 77 | ```csharp 78 | var myList = new List { 1, 2 }; 79 | var listSlice = CollectionsMarshal.AsSpan(myList); 80 | myList.Add(3); 81 | Console.Write(listSlice.Length); // This will only print 2 82 | ``` -------------------------------------------------------------------------------- /docs/advanced/lambda_methodgroup.md: -------------------------------------------------------------------------------- 1 | # Lambda vs method group 2 | This article will show the differences between a lamdbda expression and a method group. 3 | 4 | ## Prefer lambda expression over method groups 5 | Lambda expression can be cached by the .net runtime where as methods groups are not. This can lead to additional allocations if you use .net6 or below (it was improved with .net7). 6 | 7 | ❌ **Bad** Use method group. 8 | ```csharp 9 | _ints.Where(IsEven); 10 | ``` 11 | 12 | ✅ **Good** Use lambda expression. 13 | ```csharp 14 | _ints.Where(i => IsEven(i)); 15 | ``` 16 | 17 | ### Benchmark 18 | ```csharp 19 | public class LambdaVsMethodGroup 20 | { 21 | private IEnumerable _ints = Enumerable.Range(0, 100); 22 | 23 | [Benchmark(Baseline = true)] 24 | public List MethodGroup() => _ints.Where(IsEven).ToList(); 25 | 26 | [Benchmark] 27 | public List Lambda() => _ints.Where(i => IsEven(i)).ToList(); 28 | 29 | private static bool IsEven(int i) => i % 2 == 0; 30 | } 31 | ``` 32 | 33 | Results: 34 | ```csharp 35 | | Method | Mean | Error | StdDev | Ratio | RatioSD | 36 | |------------ |---------:|---------:|---------:|------:|--------:| 37 | | MethodGroup | 570.4 ns | 10.88 ns | 11.18 ns | 1.00 | 0.00 | 38 | | Lambda | 517.6 ns | 6.20 ns | 5.80 ns | 0.91 | 0.02 | 39 | ``` -------------------------------------------------------------------------------- /docs/advanced/lazy.md: -------------------------------------------------------------------------------- 1 | # `Lazy` 2 | This section will look closer into the [`Lazy`](https://docs.microsoft.com/en-us/dotnet/api/system.lazy-1?view=net-6.0) type. 3 | 4 | ## Use the correct `LazyThreadSafetyMode` option 5 | If you are using `Lazy` only in a single-threaded manner, you can save some resources when providing either the `isThreadSafe` parameter or pass in the correct [`LazyThreadSafetyMode`](https://docs.microsoft.com/en-us/dotnet/api/system.threading.lazythreadsafetymode?view=net-6.0). The default is `LazyThreadSafetyMode.ExecutionAndPublication` which creates locks to ensure the synchronization. 6 | 7 | ❌ **Bad** Use the multi-thread safe version in a single-threaded application. 8 | ```csharp 9 | var instance = new Lazy(() => new MyClass()); 10 | ``` 11 | 12 | ✅ **Good** Pass the `isThreadSafe` parameter. 13 | ```csharp 14 | var instance = new Lazy(() => new MyClass(), isThreadSafe: false); 15 | ``` 16 | 17 | ✅ **Good** Pass the correct `LazyThreadSafetyMode` parameter. 18 | ```csharp 19 | var instance = new Lazy(() => new MyClass(), LazyThreadSafetyMode.None); 20 | ``` 21 | 22 | ### Benchmark 23 | ```csharp 24 | public class LazyBenchmark 25 | { 26 | [Params( 27 | LazyThreadSafetyMode.None, 28 | LazyThreadSafetyMode.PublicationOnly, 29 | LazyThreadSafetyMode.ExecutionAndPublication)] 30 | public LazyThreadSafetyMode Option { get; set; } 31 | 32 | [Benchmark] 33 | public int Lazy() 34 | { 35 | var lazy = new Lazy(() => new MyClass(), Option); 36 | return lazy.Value.MyFunc(); 37 | } 38 | } 39 | 40 | public class MyClass 41 | { 42 | public int MyFunc() => 2; 43 | } 44 | ``` 45 | 46 | Results: 47 | ```csharp 48 | | Method | Option | Mean | Error | StdDev | 49 | |------- |--------------------- |---------:|---------:|---------:| 50 | | Lazy | None | 28.06 ns | 0.641 ns | 1.797 ns | 51 | | Lazy | PublicationOnly | 32.25 ns | 0.676 ns | 1.013 ns | 52 | | Lazy | Execu(...)ation [23] | 41.82 ns | 0.829 ns | 1.655 ns | 53 | 54 | ``` -------------------------------------------------------------------------------- /docs/advanced/span.md: -------------------------------------------------------------------------------- 1 | # `Span` 2 | This chapter will look closely to the `Span` type. The whole idea behind `Span` is to not allocate additional memory as it operates on the memory slice behind the given data type. 3 | 4 | ## Substring from a `string` 5 | Getting a substring of a `string` via the `Substring` method will create a new `string` and therefore a new allocation. 6 | 7 | ❌ **Bad** Using `Substring` to get a part of a string. 8 | ```csharp 9 | var text = "Hello World"; 10 | var hello = text.Substring(0, 5); 11 | var hello2 = text[..5]; // Range indexer uses Substring under the hood 12 | ``` 13 | 14 | ✅ **Good** Using `AsSpan` to get the underlying memory. 15 | ```csharp 16 | var text = "Hello World"; 17 | var hello = text.AsSpan().Slice(0, 5); 18 | var hello2 = text.AsSpan()[..5]; 19 | ``` 20 | 21 | ### Benchmark 22 | ```csharp 23 | [MemoryDiagnoser] 24 | public class SubstringBenchmark 25 | { 26 | private const string text = "Hello World"; 27 | 28 | [Benchmark(Baseline = true)] 29 | public string Substring() 30 | { 31 | return text[..5]; 32 | } 33 | 34 | [Benchmark] 35 | public ReadOnlySpan SpanSlice() 36 | { 37 | return text.AsSpan()[..5]; 38 | } 39 | } 40 | ``` 41 | 42 | Results: 43 | ```csharp 44 | | Method | Mean | Error | StdDev | Ratio | Gen 0 | Allocated | Alloc Ratio | 45 | |---------- |----------:|----------:|----------:|------:|-------:|----------:|------------:| 46 | | Substring | 9.9252 ns | 0.3611 ns | 1.0534 ns | 1.00 | 0.0076 | 32 B | 1.00 | 47 | | SpanSlice | 0.1921 ns | 0.0428 ns | 0.0653 ns | 0.02 | - | - | 0.00 | 48 | ``` -------------------------------------------------------------------------------- /docs/advanced/stackalloc.md: -------------------------------------------------------------------------------- 1 | # stackalloc 2 | 3 | The following chapter will show some tips and tricks around the `stackalloc` and its usage. 4 | > A `stackalloc` expression allocates a block of memory on the stack. A stack allocated memory block created during the method execution is automatically discarded when that method returns. Taken from [here](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/stackalloc) 5 | 6 | ## Allocate only small amounts of memory 7 | The **stack** has a limited size and attempting to allocate a hugerelatively speaking amount of memory and if taken too much a `StackOverflowException` is thrown which is not recoverable. Linux might have a big stack size but embedded devices can limit the size to the kilobyte range. 8 | 9 | ❌ **Bad** Allocate a huge amount. 10 | ```csharp 11 | Span = stackalloc byte[1024 * 1024]; 12 | ``` 13 | 14 | Also be careful if taken user input which can crash the program: 15 | ```csharp 16 | Span = stackalloc byte[userInput]; 17 | ``` 18 | 19 | ✅ **Good** Take small amounts of memory. 20 | ```csharp 21 | Span = stackalloc byte[1024]; 22 | ``` 23 | 24 | ✅ **Good** Fallback to traditional heap allocation if over a specific threshold. 25 | ```csharp 26 | Span = userInput <= 1024 ? stackalloc byte[1024] : new byte[userInput]; 27 | ``` 28 | 29 | ## `stackalloc` is not zero initialized 30 | In contrast to arrays allocated via `new` `stackalloc` does not zero initialize arrays. Especially if one in the caller chain uses the [`SkipLocalsInitAttribute`](https://docs.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.skiplocalsinitattribute?view=net-6.0). 31 | 32 | ❌ **Bad** Don't relay on zero initialized arrays. 33 | ```csharp 34 | Span buffer = stackalloc int[3]; 35 | buffer[0] = 1; 36 | buffer[1] = 1; 37 | 38 | int result = buffer[0] + buffer[1] + buffer[2]; // Is not necessarily 2 39 | ``` 40 | 41 | ✅ **Good** Call `Clear` or `Fill` to initialize the array if needed. 42 | ```csharp 43 | Span buffer = stackalloc int[3]; 44 | buffer.Clear(); // or buffer.Fill(0); 45 | buffer[0] = 1; 46 | buffer[1] = 1; 47 | 48 | int result = buffer[0] + buffer[1] + buffer[2]; // Is 2 49 | ``` 50 | 51 | ## `stackalloc` in a loop 52 | Every `stackalloc` gets only deallocated **after** the function terminates. That means in a loop multiple `stackalloc` will pill up memory which ultimately can lead to an `StackOverflowException`. Furthermore it is inefficient to allocate always the same amount of memory. 53 | 54 | ❌ **Bad** Allocating over and over again the "same" memory inside a loop pilling up stack memory. 55 | ```csharp 56 | for (int i = 0; i < 1000; ++i) 57 | { 58 | Span buffer = stackalloc char[256]; 59 | // ... 60 | } 61 | ``` 62 | 63 | ✅ **Good** Create one slice of memory outside the loop. 64 | ```csharp 65 | Span buffer = stackalloc char[256]; 66 | for (int i = 0; i < 1000; ++i) 67 | { 68 | // ... 69 | } 70 | ``` -------------------------------------------------------------------------------- /docs/advanced/struct.md: -------------------------------------------------------------------------------- 1 | # struct 2 | `struct`s can have a positive impact on performance due to their nature of living on the stack instead of the heap. 3 | Of course `struct` will be put onto the heap if they outlive their stack frame. 4 | 5 | ## Passing by value can be faster than by reference 6 | If the `struct` is small enough (at best as wide as a machine word, so 8 bytes on 64bit applications or 4 bytes on 32 bit applications) passing a `struct` by reference can be significant faster than any reference. 7 | The reason is that copying a `struct` on the stack is a cheap operation when the `struct` is small enough. A reference in contrast is always as wide as the machine word but on top, for reading values, it has to be dereferences. 8 | 9 | > 💡 Info: In detail explanation can be found [here](https://steven-giesel.com/blogPost/d205e99c-e784-49be-a2e6-7f9c44ab890f). 10 | 11 | ## Allocation of small `struct`s are cheaper than reference types 12 | Heap allocation is more expensive than creating an object on the stack. 13 | If used in loops with only local usage of the (small) `struct` they can improve the performance. 14 | 15 | ❌ **Bad** Classes are relatively expensive 16 | ```csharp 17 | public void SlimClass() 18 | { 19 | var array = new SlimClass[1000]; 20 | for (var i = 0; i < 1000; i++) 21 | array[i] = new SlimClass(); 22 | } 23 | ``` 24 | 25 | ✅ **Good** Structs are cheap to create 26 | ```csharp 27 | public void SlimStruct() 28 | { 29 | var array = new SlimStruct[1000]; 30 | for (var i = 0; i < 1000; i++) 31 | array[i] = new SlimStruct(); 32 | } 33 | ``` 34 | 35 | ⚠️ **Warning** If the array of a struct is larger than 85kb it will be moved to the **L**arge **O**bject **H**eap which can give a major performance penalty. 36 | 37 | ### Benchmark 38 | ```csharp 39 | [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory), CategoriesColumn] 40 | public class RegexBenchmark 41 | { 42 | [Params(10, 1_000)] 43 | public int ObjectsToCreate { get; set; } 44 | 45 | [Benchmark(Baseline = true), BenchmarkCategory("Slim")] 46 | public void SlimClass() 47 | { 48 | var array = new SlimClass[ObjectsToCreate]; 49 | for (var i = 0; i < ObjectsToCreate; i++) 50 | array[i] = new SlimClass(); 51 | } 52 | 53 | [Benchmark, BenchmarkCategory("Slim")] 54 | public void SlimStruct() 55 | { 56 | var array = new SlimStruct[ObjectsToCreate]; 57 | for (var i = 0; i < ObjectsToCreate; i++) 58 | array[i] = new SlimStruct(); 59 | } 60 | 61 | [Benchmark(Baseline = true), BenchmarkCategory("Fat")] 62 | public void FatClass() 63 | { 64 | var array = new FatClass[ObjectsToCreate]; 65 | for (var i = 0; i < ObjectsToCreate; i++) 66 | array[i] = new FatClass(); 67 | } 68 | 69 | [Benchmark, BenchmarkCategory("Fat")] 70 | public void FatStruct() 71 | { 72 | var array = new FatStruct[ObjectsToCreate]; 73 | for (var i = 0; i < ObjectsToCreate; i++) 74 | array[i] = new FatStruct(); 75 | } 76 | } 77 | 78 | public class SlimClass { } 79 | public struct SlimStruct { } 80 | 81 | public class FatClass 82 | { 83 | public int A, B, C, D, E, F, G, H, I, J, K, L, M, N, Q, R, S, T, U, V, W, X, Y, Z; 84 | } 85 | 86 | public struct FatStruct 87 | { 88 | public int A, B, C, D, E, F, G, H, I, J, K, L, M, N, Q, R, S, T, U, V, W, X, Y, Z; 89 | } 90 | ``` 91 | 92 | Results: 93 | ```csharp 94 | | Method | Categories | ObjectsToCreate | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | Alloc Ratio | 95 | |----------- |----------- |---------------- |--------------:|------------:|------------:|------:|--------:|--------:|--------:|--------:|----------:|------------:| 96 | | FatClass | Fat | 10 | 248.246 ns | 7.1119 ns | 20.2908 ns | 1.00 | 0.00 | 0.2923 | - | - | 1224 B | 1.00 | 97 | | FatStruct | Fat | 10 | 71.918 ns | 2.1169 ns | 6.0054 ns | 0.29 | 0.03 | 0.2352 | - | - | 984 B | 0.80 | 98 | | | | | | | | | | | | | | | 99 | | FatClass | Fat | 1000 | 11,551.343 ns | 231.0578 ns | 247.2293 ns | 1.00 | 0.00 | 28.6865 | 0.0305 | - | 120024 B | 1.00 | 100 | | FatStruct | Fat | 1000 | 54,708.078 ns | 362.9784 ns | 321.7709 ns | 4.73 | 0.12 | 30.2734 | 30.2734 | 30.2734 | 96034 B | 0.80 | 101 | | | | | | | | | | | | | | | 102 | | SlimClass | Slim | 10 | 60.762 ns | 1.1299 ns | 1.6562 ns | 1.00 | 0.00 | 0.0823 | - | - | 344 B | 1.00 | 103 | | SlimStruct | Slim | 10 | 9.246 ns | 0.1056 ns | 0.0936 ns | 0.15 | 0.01 | 0.0096 | - | - | 40 B | 0.12 | 104 | | | | | | | | | | | | | | | 105 | | SlimClass | Slim | 1000 | 5,968.366 ns | 111.5320 ns | 271.4843 ns | 1.00 | 0.00 | 7.6599 | - | - | 32024 B | 1.00 | 106 | | SlimStruct | Slim | 1000 | 484.738 ns | 8.7875 ns | 8.6305 ns | 0.08 | 0.00 | 0.2441 | - | - | 1024 B | 0.03 | 107 | 108 | ``` 109 | 110 | ## Override equals and operator equals on value types 111 | When comparing a custom `struct` with each other dotnet will use reflection to achieve comparison. In performance critical paths this might not be desirable. 112 | 113 | ❌ **Bad** Don't provide overrides for `Equals` and similar operations. 114 | ```csharp 115 | public struct Point 116 | { 117 | public int X { get; set; } 118 | public int Y { get; set; } 119 | } 120 | 121 | var p1 = default(Point); 122 | var p2 = default(Point); 123 | var isSame = p1 == p2; // Uses reflection to achieve comparison 124 | ``` 125 | 126 | ✅ **Good** Provide explicit implementations. 127 | ```csharp 128 | public struct Point : IEquatable // Implementing the interface is optional 129 | { 130 | public int X { get; set; } 131 | public int Y { get; set; } 132 | 133 | public override int GetHashCode() => ... 134 | public override bool Equals(object obj) => ... 135 | public bool Equals(Point p2) => ... 136 | 137 | public static bool operator ==(Point point1, Point point2) => point1.Equals(point2); 138 | public static bool operator !=(Point point1, Point point2) => !point1.Equals(point2); 139 | } 140 | ``` 141 | 142 | > 💡 Info: `record struct`, which were introduced with C# 10, automatically implement `IEquatable`. So by using a `record struct` you get some performance benefits. A more in-depth analysis can be found [here](https://steven-giesel.com/blogPost/073f2a14-ea0f-4241-9729-41ee0f30f90c). 143 | 144 | ### Benchmark 145 | ```csharp 146 | public class ValueTypeEquals 147 | { 148 | private readonly PointNoOverride _noOverrideP1 = new(); 149 | private readonly PointNoOverride _noOverrideP2 = new(); 150 | private readonly PointRecord _pointRecordP1 = new(0, 0); 151 | private readonly PointRecord _pointRecordP2 = new(0, 0); 152 | 153 | [Benchmark(Baseline = true)] 154 | public bool IsSameNoOverride() => _noOverrideP1.Equals(_noOverrideP2); 155 | 156 | [Benchmark] 157 | public bool IsSameOverride() => _pointRecordP1.Equals(_pointRecordP2); 158 | } 159 | 160 | public struct PointNoOverride 161 | { 162 | public int X { get; init; } 163 | public int Y { get; init; } 164 | } 165 | // record struct implements IEquatable 166 | public record struct PointRecord(int X, int Y); 167 | ``` 168 | 169 | Results: 170 | ```csharp 171 | | Method | Mean | Error | StdDev | Ratio | Gen 0 | Allocated | Alloc Ratio | 172 | |----------------- |-----------:|----------:|----------:|------:|-------:|----------:|------------:| 173 | | IsSameNoOverride | 24.5980 ns | 0.7290 ns | 2.0682 ns | 1.00 | 0.0115 | 48 B | 1.00 | 174 | | IsSameOverride | 0.6466 ns | 0.0499 ns | 0.0555 ns | 0.03 | - | - | 0.00 | 175 | ``` 176 | 177 | ## Override `GetHashCode` when used in a `Dictionary` 178 | When a custom defined `struct` is used as a key in a `Dictionary` reflection is used to calculate the has of the current object. In performance critical paths that is not desirable. 179 | 180 | ❌ **Bad** Rely on the reflection to calculate the hash 181 | ```csharp 182 | public struct Point 183 | { 184 | public int X { get; set; } 185 | public int Y { get; set; } 186 | } 187 | 188 | var dictionary = new Dictionary(); 189 | ... 190 | var worldObjectAtP1 = dictionary[p1]; 191 | ``` 192 | 193 | ✅ **Good** Provide `GetHashCode` implementation. 194 | ```csharp 195 | public struct Point 196 | { 197 | public int X { get; set; } 198 | public int Y { get; set; } 199 | 200 | public override int GetHashCode() => HashCode.Combine(X, Y); 201 | } 202 | 203 | var dictionary = new Dictionary(); 204 | ... 205 | var worldObjectAtP1 = dictionary[p1]; 206 | ``` 207 | 208 | > 💡 Info: `record struct`, which were introduced with C# 10, automatically implement a non reflective `GetHashCode`. So by using a `record struct` you get some performance benefits. A more in-depth analysis can be found [here](https://steven-giesel.com/blogPost/073f2a14-ea0f-4241-9729-41ee0f30f90c). 209 | 210 | ### Benchmark 211 | ```csharp 212 | [MemoryDiagnoser] 213 | public class StructGetHashCode 214 | { 215 | private static readonly PointNoOverride pointNoOverride = new(); 216 | private static readonly PointRecord pointOverride = new(); 217 | private readonly Dictionary dictionaryNoOverride = new() 218 | { 219 | { pointNoOverride, 1 } 220 | }; 221 | private readonly Dictionary dictionaryOverride = new() 222 | { 223 | { pointOverride, 1 } 224 | }; 225 | 226 | [Benchmark(Baseline = true)] 227 | public int GetFromNoOverride() => dictionaryNoOverride[pointNoOverride]; 228 | 229 | [Benchmark] 230 | public int GetFromOverride() => dictionaryOverride[pointOverride]; 231 | } 232 | 233 | public struct PointNoOverride 234 | { 235 | public int X { get; init; } 236 | public int Y { get; init; } 237 | } 238 | // record struct implements GetHashCode 239 | public record struct PointRecord(int X, int Y); 240 | ``` 241 | 242 | Results: 243 | ```csharp 244 | | Method | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen 0 | Allocated | Alloc Ratio | 245 | |------------------ |---------:|---------:|---------:|---------:|------:|--------:|-------:|----------:|------------:| 246 | | GetFromNoOverride | 57.35 ns | 1.303 ns | 3.801 ns | 56.31 ns | 1.00 | 0.00 | 0.0172 | 72 B | 1.00 | 247 | | GetFromOverride | 19.45 ns | 0.413 ns | 0.386 ns | 19.30 ns | 0.33 | 0.02 | - | - | 0.00 | 248 | ``` -------------------------------------------------------------------------------- /docs/array.md: -------------------------------------------------------------------------------- 1 | # Array 2 | This page will look into arrays and their usage. 3 | 4 | ## Prefer jagged arrays over multidimensional 5 | In a multidimensional array, each element in each dimension has the same, fixed size as the other elements in that dimension. In a jagged array, which is an array of arrays, each inner array can be of a different size. By only using the space that's needed for a given array, no space is wasted. Also from a performance point of view a jagged array performs generally better than a multidimensional array. 6 | 7 | ❌ **Bad** Multidimensional array performs less optimal and can waste space. 8 | ```csharp 9 | int[,] multiDimArray = { {1, 2}, {3, 4}}; 10 | ``` 11 | 12 | ✅ **Good** Jagged array performs generally better and only uses the really needed space. 13 | ```csharp 14 | int[][] jaggedArray = { new int[] {1, 2}, new int [] {3, 4}}; 15 | ``` 16 | 17 | ### Benchmark 18 | ```csharp 19 | [MemoryDiagnoser] 20 | public class Arrays 21 | { 22 | [Params(25, 100)] 23 | public int Size { get; set; } 24 | 25 | [Benchmark] 26 | public int[][] CreateAndFillJaggedArray() 27 | { 28 | int[][] jaggedArray = new int[Size][]; 29 | for (var i = 0; i < Size; i++) 30 | { 31 | jaggedArray[i] = new int[Size]; 32 | for (var j = 0; j < Size; j++) jaggedArray[i][j] = i + j; 33 | } 34 | 35 | return jaggedArray; 36 | } 37 | 38 | [Benchmark] 39 | public int[,] CreateAndFillMultidimensionalArray() 40 | { 41 | int[,] multidimArray = new int[Size, Size]; 42 | for (var i = 0; i < Size; i++) 43 | { 44 | for (var j = 0; j < Size; j++) 45 | multidimArray[i, j] = i + j; 46 | } 47 | 48 | return multidimArray; 49 | } 50 | } 51 | ``` 52 | 53 | Result: 54 | ```csharp 55 | | Method | Size | Mean | Error | StdDev | Median | Gen 0 | Gen 1 | Allocated | 56 | |----------------------------------- |----- |------------:|----------:|----------:|------------:|--------:|-------:|----------:| 57 | | CreateAndFillJaggedArray | 25 | 717.7 ns | 15.51 ns | 43.50 ns | 700.3 ns | 0.8183 | - | 3.34 KB | 58 | | CreateAndFillMultidimensionalArray | 25 | 1,480.2 ns | 29.49 ns | 82.21 ns | 1,463.7 ns | 0.6065 | - | 2.48 KB | 59 | | CreateAndFillJaggedArray | 100 | 9,895.6 ns | 143.22 ns | 126.96 ns | 9,900.8 ns | 10.3302 | 0.0153 | 42.21 KB | 60 | | CreateAndFillMultidimensionalArray | 100 | 16,374.7 ns | 319.00 ns | 567.02 ns | 16,311.2 ns | 9.5215 | 0.0305 | 39.1 KB | 61 | ``` 62 | 63 | ## Locality matters 64 | Memory location matters. Accessing memory location sequentially as opposed to "jumping" around to read some memory address. 65 | 66 | ❌ **Bad** Do not access memory sequentially. 67 | ```csharp 68 | public int NotLocal() 69 | { 70 | var sum = 0; 71 | for (var i = 0; i < _table.GetLength(1); i++) 72 | { 73 | for (var j = 0; j < _table.GetLength(0); j++) 74 | sum += _table[j, i]; 75 | } 76 | 77 | return sum; 78 | } 79 | ``` 80 | 81 | ✅ **Good** Accessing memory sequentially. 82 | ```csharp 83 | public int Local() 84 | { 85 | var sum = 0; 86 | for (var i = 0; i < _table.GetLength(0); i++) 87 | { 88 | for (var j = 0; j < _table.GetLength(1); j++) 89 | sum += _table[i, j]; 90 | } 91 | 92 | return sum; 93 | } 94 | ``` 95 | 96 | ### Benchmark 97 | Results for the given example above: 98 | ```csharp 99 | | Method | Mean | Error | StdDev | Ratio | RatioSD | 100 | |--------- |---------:|--------:|--------:|------:|--------:| 101 | | Local | 120.8 us | 2.35 us | 2.70 us | 1.00 | 0.00 | 102 | | NotLocal | 158.6 us | 3.09 us | 3.56 us | 1.31 | 0.05 | 103 | ``` -------------------------------------------------------------------------------- /docs/async_await.md: -------------------------------------------------------------------------------- 1 | # async await 2 | The following chapter will deep dive into tips, tricks and pitfalls when it comes down to `async` and `await` in C#. 3 | 4 | ## Elide await keyword - Exceptions 5 | Eliding the await keyword can lead to a less traceable stacktrace due to the fact that every `Task` which doesn't get awaited, will not be part of the stack trace. 6 | 7 | ❌ **Bad** 8 | ```csharp 9 | using System; 10 | using System.Threading.Tasks; 11 | 12 | try 13 | { 14 | await DoWorkWithoutAwaitAsync(); 15 | } 16 | catch (Exception e) 17 | { 18 | Console.WriteLine(e); 19 | } 20 | 21 | static Task DoWorkWithoutAwaitAsync() 22 | { 23 | return ThrowExceptionAsync(); 24 | } 25 | 26 | static async Task ThrowExceptionAsync() 27 | { 28 | await Task.Yield(); 29 | throw new Exception("Hey"); 30 | } 31 | ``` 32 | 33 | Will result in 34 | 35 | ``` 36 | System.Exception: Hey 37 | at Program.<
$>g__ThrowExceptionAsync|0_1() 38 | at Program.
$(String[] args) 39 | ``` 40 | 41 | ✅ **Good** If speed and allocation is not very crucial, add the `await` keyword. 42 | ```csharp 43 | using System; 44 | using System.Threading.Tasks; 45 | 46 | try 47 | { 48 | await DoWorkWithoutAwaitAsync(); 49 | } 50 | catch (Exception e) 51 | { 52 | Console.WriteLine(e); 53 | } 54 | 55 | static async Task DoWorkWithoutAwaitAsync() 56 | { 57 | await ThrowExceptionAsync(); 58 | } 59 | 60 | static async Task ThrowExceptionAsync() 61 | { 62 | await Task.Yield(); 63 | throw new Exception("Hey"); 64 | } 65 | ``` 66 | 67 | Will result in: 68 | ``` 69 | System.Exception: Hey 70 | at Program.<
$>g__ThrowExceptionAsync|0_1() 71 | at Program.<
$>g__DoWorkWithoutAwaitAsync|0_0() 72 | at Program.
$(String[] args) 73 | ``` 74 | 75 | > 💡 Info: Eliding the `async` keyword will also elide the whole state machine. In very hot paths that might be worth a consideration. In normal cases one should not elide the keyword. The allocations one is saving is depending on the circumstances but a normally very very small especially if only smaller objects are passed around. Also performance-wise there is no big gain when eliding the keyword (we are talking nano seconds). Please measure first and act afterwards. 76 | 77 | ## Elide await keyword - using block 78 | Eliding inside an `using` block can lead to a disposed object before the `Task` is finished. 79 | 80 | ❌ **Bad** Here the download will be aborted / the `HttpClient` gets disposed: 81 | ```csharp 82 | public Task GetContentFromUrlAsync(string url) 83 | { 84 | using var client = new HttpClient(); 85 | return client.GetStringAsync(url); 86 | } 87 | ``` 88 | 89 | ✅ **Good** 90 | ```csharp 91 | public async Task GetContentFromUrlAsync(string url) 92 | { 93 | using var client = new HttpClient(); 94 | return await client.GetStringAsync(url); 95 | } 96 | ``` 97 | 98 | > 💡 Info: Eliding the `async` keyword will also elide the whole state machine. In very hot paths that might be worth a consideration. In normal cases one should not elide the keyword. The allocations one is saving is depending on the circumstances but a normally very very small especially if only smaller objects are passed around. Also performance-wise there is no big gain when eliding the keyword (we are talking nano seconds). Please measure first and act afterwards. 99 | 100 | ## Return `null` `Task` or `Task` 101 | When returning directly `null` from a synchronous call (no `async` or `await`) will lead to `NullReferenceException`: 102 | 103 | ❌ **Bad** Will throw `NullReferenceException` 104 | ```csharp 105 | await GetAsync(); 106 | 107 | static Task GetAsync() 108 | { 109 | return null; 110 | } 111 | ``` 112 | 113 | ✅ **Good** Use `Task.FromResult`: 114 | 115 | ```csharp 116 | await GetAsync(); 117 | 118 | static Task GetAsync() 119 | { 120 | return Task.FromResult(null); 121 | } 122 | ``` 123 | 124 | ## `async void` 125 | The problem with `async void` is first they are not awaitable and second they suffer the same problem with exceptions and stack trace as discussed a bit earlier. It is basically fire and forget. 126 | 127 | ❌ **Bad** Not awaited 128 | ```csharp 129 | public async void DoAsync() 130 | { 131 | await SomeAsyncOp(); 132 | } 133 | ``` 134 | 135 | ✅ **Good** return `Task` instead of `void` 136 | ```csharp 137 | public async Task DoAsync() 138 | { 139 | await SomeAsyncOp(); 140 | } 141 | ``` 142 | 143 | > 💡 Info: There are valid cases for `async void` like top level event handlers. 144 | 145 | ## `List.ForEach` with `async` 146 | `List.ForEach` and in general a lot of LINQ methods don't go well with `async` `await`: 147 | 148 | ❌ **Bad** Is the same as `async void` 149 | ```csharp 150 | var ids = new List(); 151 | // ... 152 | ids.ForEach(id => _myRepo.UpdateAsync(id)); 153 | ``` 154 | 155 | One could thing adding `async` into the lamdba would do the trick: 156 | 157 | ❌ **Bad** Still the same as `async void` because `List.ForEach` takes an `Action` and not a `Func`. 158 | ```csharp 159 | var ids = new List(); 160 | // ... 161 | ids.ForEach(async id => await _myRepo.UpdateAsync(id)); 162 | ``` 163 | 164 | ✅ **Good** Enumerate through the list via `foreach` 165 | ```csharp 166 | foreach (var id in ids) 167 | { 168 | await _myRepo.UpdateAsync(id); 169 | } 170 | ``` 171 | 172 | ## Favor `await` over synchronous calls 173 | Using blocking calls instead of `await` can lead to potential deadlocks and other side effects like a poor stack trace in case of an exception and less scalability in web frameworks like ASP.NET core. 174 | 175 | ❌ **Bad** This call blocks the thread. 176 | ```csharp 177 | public async Task SomeOperationAsync() 178 | { 179 | await ... 180 | } 181 | 182 | public void Do() 183 | { 184 | SomeOperationAsync().Wait(); 185 | } 186 | ``` 187 | 188 | ✅ **Good** Use `async` & `await` in the whole chain 189 | ```csharp 190 | public async Task SomeOperationAsync() 191 | { 192 | await ... 193 | } 194 | 195 | public async Task Do() 196 | { 197 | await SomeOperationAsync(); 198 | } 199 | ``` 200 | 201 | ## Favor `GetAwaiter().GetResult()` over `Wait` and `Result` 202 | `Task.GetAwaiter().GetResult()` is preferred over `Task.Wait` and `Task.Result` because it propagates exceptions rather than wrapping them in an AggregateException. 203 | 204 | ❌ **Bad** 205 | ```csharp 206 | string content = DownloadAsync().Result; 207 | ``` 208 | 209 | ✅ **Good** 210 | ```csharp 211 | string content = DownloadAsync().GetAwaiter().GetResult(); 212 | ``` 213 | 214 | ## Don't use `Task.Delay` for small precise waiting times 215 | [`Task.Delay`](https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.delay?view=net-6.0#System_Threading_Tasks_Task_Delay_System_Int32_)'s internal timer is dependent on the underlying OS. On most windows machines this resolution is about 15ms. 216 | 217 | So: `Task.Delay(1)` will not wait one millisecond but something between one and 15 milliseconds. 218 | 219 | ```csharp 220 | var stopwatch = Stopwatch.StartNew(); 221 | await Task.Delay(1); 222 | stopwatch.Stop(); // Don't account the Console.WriteLine into the timer 223 | Console.WriteLine($"Delay was {stopwatch.ElapsedMilliseconds} ms"); 224 | ``` 225 | 226 | Will print for example: 227 | > Delay was 6 ms 228 | 229 | ## Properly awaiting concurrent tasks 230 | Often times tasks are independent of each other and can be awaited independently. 231 | 232 | ❌ **Bad** The following code will run roughly 1 second. 233 | ```csharp 234 | 235 | await DoOperationAsync(); 236 | await DoOperationAsync(); 237 | 238 | async Task DoOperationAsync() 239 | { 240 | await Task.Delay(500); 241 | } 242 | ``` 243 | 244 | ✅ **Good** When tasks or their data is independent they can be awaited independently for maximum benefits. The following code will run roughly 0.5 seconds. 245 | 246 | ```csharp 247 | var t1 = DoOperationAsync(); 248 | var t2 = DoOperationAsync(); 249 | await t1; 250 | await t2; 251 | 252 | async Task DoOperationAsync() 253 | { 254 | await Task.Delay(500); 255 | } 256 | ``` 257 | 258 | An alternative to this would be [`Task.WhenAll`](https://docs.microsoft.com/en-us/documentation/): 259 | ```csharp 260 | var t1 = DoOperationAsync(); 261 | var t2 = DoOperationAsync(); 262 | await Task.WhenAll(t1, t2); // Can also be inlined 263 | 264 | async Task DoOperationAsync() 265 | { 266 | await Task.Delay(500); 267 | } 268 | ``` 269 | 270 | ## `ConfigureAwait` with `await using` statement 271 | Since C# 8 you can provide an `IAsyncDisposable` which allows to have asynchrnous code in the `Dispose` method. Also this allows to call the following construct: 272 | 273 | ```csharp 274 | await using var dbContext = await dbContextFactory.CreateDbContextAsync().ConfigureAwait(false); 275 | ``` 276 | 277 | In this example `CreateDbContextAsync` uses the `ConfigureAwait(false)` but **not** the `IAsyncDisposable`. To make that work we have to break apart the statment like this: 278 | 279 | ```csharp 280 | var dbContext = await dbContextFactory.CreateDbContextAsync().ConfigureAwait(false); 281 | await using (dbContext.ConfigureAwait(false)) 282 | { 283 | // ... 284 | } 285 | ``` 286 | 287 | The last part has the "ugly" snippet that you have to introduce a new "block" for the `using` statement. For that there is a easy workaround: 288 | ```csharp 289 | var blogDbContext = await dbContextFactory.CreateDbContextAsync().ConfigureAwait(false); 290 | await using var _ = blogDbContext.ConfigureAwait(false); 291 | // You don't need the {} block here 292 | ``` 293 | 294 | ## Avoid Async in Constructors 295 | Constructors are meant to initialize objects synchronously, as their primary purpose is to set up the initial state of the object. When you need to perform asynchronous operations during object initialization, using async methods in constructors can lead to issues such as deadlocks or incorrect object initialization. Instead, use a static asynchronous factory method or a separate asynchronous initialization method to ensure safe and proper object initialization. 296 | 297 | Using async operations in constructors can be problematic for several reasons like **Deadlocks**, **Incomplete Initialization**, and **Exception Handling**. 298 | 299 | ❌ **Bad** Calling async methods in constructors 300 | 301 | ```csharp 302 | public class MyClass 303 | { 304 | public MyClass() 305 | { 306 | InitializeAsync().GetAwaiter().GetResult(); 307 | } 308 | 309 | private async Task InitializeAsync() => await LoadDataAsync(); 310 | ``` 311 | 312 | ✅ **Good**: Option 1 - Static asynchronous factory method: 313 | 314 | ```csharp 315 | public class MyClass 316 | { 317 | private MyClass() { } 318 | 319 | public static async Task CreateAsync() 320 | { 321 | var instance = new MyClass(); 322 | await instance.InitializeAsync(); 323 | return instance; 324 | } 325 | 326 | private async Task InitializeAsync() => await LoadDataAsync(); 327 | ``` 328 | 329 | ✅ **Good**: Option 2 - Separate asynchronous initialization method: 330 | 331 | ```csharp 332 | public class MyClass 333 | { 334 | public async Task InitializeAsync() => await LoadDataAsync(); 335 | ``` -------------------------------------------------------------------------------- /docs/blazor.md: -------------------------------------------------------------------------------- 1 | # Blazor 2 | The following chapter show some tips and tricks in regards to **Blazor**. 3 | 4 | ## `CascadingValue` fixed 5 | CascadingValues are used, as the name implies, the cascade a parameter down the hierarchy. The problem with that is, that every child component will listen to changes to the original value. This can get expensive. If your component does not rely on updates, you can pin / fix this value. For that you can use the IsFixed parameter. 6 | 7 | ❌ **Bad** in case `SomeOtherComponents` does not need update of the cascading value 8 | ```razor 9 | 10 | 11 | 12 | ``` 13 | 14 | ✅ **Good** If the component does **not** need further updates (or the value never changes anyway) we can **fix** the value. 15 | ```razor 16 | 17 | 18 | 19 | ``` 20 | 21 | ## `@key` directive 22 | Blazor diff engine determines which elements have to be re-rendered on every render cycle. It does that via sequence numbers. That works in most of cases very well, but adding or removing items in the middle or at the beginning of the list, will lead to re-rendering the whole list instead of the single entry. 23 | 24 | ❌ **Bad** in case we entries are added in the middle or at the beginning. 25 | ```razor 26 |
    27 | @foreach (var item in items) 28 | { 29 |
  • @item
  • 30 | } 31 |
32 | ``` 33 | 34 | ✅ **Good** Helping Blazor how it should find the difference between two render cycles. 35 | ```razor 36 |
    37 | @foreach (var item in items) 38 | { 39 | @* With the @key attribute we define what makes the element unique 40 | Based on this Blazor can determine whether or not it has to re-render 41 | the element 42 | *@ 43 |
  • @item
  • 44 | } 45 |
46 | ``` 47 | 48 | > 💡 Info: If you want to know more about `@key` [here](https://steven-giesel.com/blogPost/a8772410-847d-4fe7-ba93-3e03ab7748c0) is article about that topic. 49 | 50 | ## `StateHasChanged` from async call without `InvokeAsync` 51 | In the current state (.net6 / .net7.0 preview 4) Blazor WASM only runs on one thread at the time. Therefore switching context via `await` will still be run on the main UI thread. Once Blazor WASM introduces real threads, code which invokes UI changes from a background thread will throw an exception. For Blazor Server this holds true today, even though the `SynchronizationContext` tries to be as much as possible single threaded. 52 | 53 | ❌ **Bad** Can lead to exceptions on Blazor Server now or Blazor WASM in the future. 54 | ```csharp 55 | protected override void OnInitialized() 56 | { 57 | base.OnInitialized(); 58 | Timer = new System.Threading.Timer(_ => 59 | { 60 | StateHasChanged(); 61 | }, null, 500, 500); 62 | } 63 | ``` 64 | 65 | ✅ **Good** Call `InvokeAsync` to force rendering on the main UI thread. 66 | ```csharp 67 | protected override void OnInitialized() 68 | { 69 | base.OnInitialized(); 70 | Timer = new System.Threading.Timer(_ => 71 | { 72 | InvokeAsync(StateHasChanged); 73 | }, null, 500, 500); 74 | } 75 | ``` 76 | 77 | ## Use `JSImport` or `JSExport` attributes to simplify the interop 78 | With .NET 7 developers can use `JSImportAttribute` to automatically import a javascript function into dotnet and use it as regular C# function. Also the opposite is possible. This only works on client-side Blazor (also called Blazor WASM). 79 | 80 | ```csharp 81 | // We have to mark our class partial otherwise SourceCodeGenerator can't add code 82 | public partial class MyComponent 83 | { 84 | // Super easy way to import a Javascript function to .NET 85 | [JSImport("console.log")] 86 | static partial void Log(string message); 87 | 88 | // We can also export functions, so that we can use them in JavaScript 89 | // Dotnet will take care of marshalling 90 | [JSExport] 91 | [return: JSMarshalAs] 92 | public static DateTime Now() 93 | { 94 | return DateTime.Now; 95 | } 96 | 97 | // The imported functions can be called like every other .NET function 98 | ``` 99 | 100 | ## Cancelling a navigation 101 | Since .net 7 it is possible to intercept a navigation request, which either goes to an internal or external page. For that we can utilize the `NavigationLock` component. It distinguishes between internal and external calls. External calls are always handled via a JS popup. The same can be used for internal calls as well. 102 | 103 | ```csharp 104 | @inject IJSRuntime JSRuntime 105 | 106 | 107 | 108 | @code { 109 | private async Task OnBeforeInternalNavigation(LocationChangingContext context) 110 | { 111 | // This just displays the built-in browser confirm dialog, but you can display a custom prompt 112 | // for internal navigations if you want. 113 | var isConfirmed = await JSRuntime.InvokeAsync("confirm", "Are you sure you want to continue?"); 114 | 115 | if (!isConfirmed) 116 | { 117 | context.PreventNavigation(); 118 | } 119 | } 120 | } 121 | ``` -------------------------------------------------------------------------------- /docs/collections.md: -------------------------------------------------------------------------------- 1 | # Collections 2 | This chapter looks closer to collections and its best practices. 3 | 4 | ## Passing collections as a method parameter 5 | 6 | If your method needs to accept a collection as a parameter, then generally `IEnumerable` is ok and this offers the most flexibility to the caller. It doesn't matter what concrete collection type they use, and it even allows them to pass lazily evaluated sequences in, This is a `general` approach and for preventing multiple enumeration we can call `ToList()` in first of executing method or we can use `IReadOnlyCollection` or `IReadOnlyList` for preventing multiple enumeration. 7 | 8 | ❌ **Bad** using `IEnumerable` generally as input collection type is ok, but without using `ToList()` in first of executing could create multiple enumeration. 9 | ```csharp 10 | void PrintProducts(IEnumerable products) 11 | { 12 | // First Enumeration. 13 | Console.WriteLine($"Product Count is: {products.Count()}."); 14 | 15 | // Second Enumeration. 16 | foreach(var product in products) 17 | { 18 | Console.WriteLine($"Id: {product.Id} - Title: {product.Title}"); 19 | } 20 | } 21 | 22 | void Caller() 23 | { 24 | PrintProducts(products.Where(p => p.Title == "IPhone")); 25 | } 26 | ``` 27 | 28 | ✅ **Good** With using `IReadOnlyCollection` in our parameter specifically, we can prevent unexpected behaviors and multiple enumerations. 29 | ```csharp 30 | void PrintProducts(IReadOnlyCollection products) 31 | { 32 | Console.WriteLine($"Product Count is: {products.Count}."); 33 | 34 | foreach(var product in products) 35 | { 36 | Console.WriteLine($"Id: {product.Id} - Title: {product.Title}"); 37 | } 38 | } 39 | 40 | void Caller() 41 | { 42 | PrintProducts(products.where(p => p.Title == "IPhone").AsReadOnly()); 43 | } 44 | ``` 45 | 46 | ✅ **Good** With using `IEnumerable` in our parameter specifically and calling `ToList()` in first line of executing, we can prevent unexpected behaviors and multiple enumerations. 47 | ```csharp 48 | void PrintProducts(IEnumerable products) 49 | { 50 | var items = products.ToList(); 51 | 52 | Console.WriteLine($"Product Count is: {items.Count}."); 53 | 54 | foreach(var product in items) 55 | { 56 | Console.WriteLine($"Id: {product.Id} - Title: {product.Title}"); 57 | } 58 | } 59 | 60 | void Caller() 61 | { 62 | PrintProducts(products.Where(p => p.Title == "IPhone")); 63 | } 64 | ``` 65 | 66 | ## Possible multiple enumeration with `IEnumerable` 67 | As `IEnumerable` is not a materialized collection but more cursor on the current element of the enumeration, using multiple functions on `IEnumerable` can lead to possible multiple enumerations. 68 | This becomes a penalty when retrieving the content is expensive as the work is done multiple times, for example it could be an expensive operation that goes to a database multiple time with each enumeration, or it could even return different results in each enumeration. 69 | 70 | ❌ **Bad** Will enumerate the enumeration twice 71 | ```csharp 72 | IEnumerable integers = GetIntegers(); 73 | var min = integers.Min(); 74 | var max = integers.Max(); 75 | 76 | Console.Write($"Min: {min} / Max: {max}"); 77 | 78 | IEnumerable GetIntegers() 79 | { 80 | Console.WriteLine("Getting integer"); 81 | yield return 1; 82 | yield return 2; 83 | } 84 | ``` 85 | 86 | Will output: 87 | > Getting integer 88 | Getting integer 89 | Min: 1 / Max: 2 90 | 91 | ✅ **Good** Enumeration get materialized once via `ToList` 92 | ```csharp 93 | IEnumerable integers = GetIntegers(); 94 | var integerList = integers.ToList(); 95 | var min = integerList.Min(); 96 | var max = integerList.Max(); 97 | 98 | Console.Write($"Min: {min} / Max: {max}"); 99 | 100 | IEnumerable GetIntegers() 101 | { 102 | Console.WriteLine("Getting integer"); 103 | yield return 1; 104 | yield return 2; 105 | } 106 | ``` 107 | 108 | Will output: 109 | > Getting integer 110 | Min: 1 / Max: 2 111 | 112 | ## Returning null for `IEnumerable` 113 | A lot of **LINQ** operations are built upon the premise that `IEnumerable`, even though it is a reference type, should not be `null` at any given time. 114 | 115 | ❌ **Bad** Instead of an empty enumeration a `null` will be returned which can confuse a consumer. 116 | ```csharp 117 | public IEnumerable GetAll() 118 | { 119 | if (...) 120 | { 121 | return null; 122 | } 123 | ... 124 | } 125 | ``` 126 | 127 | ✅ **Good** Instead return an empty enumeration. 128 | ```csharp 129 | public IEnumerable GetAll() 130 | { 131 | if (...) 132 | { 133 | return Enumerable.Empty(); 134 | } 135 | ... 136 | } 137 | ``` 138 | 139 | ## Consider `IReadOnlyCollection` if you always return an in memory list 140 | 141 | Many developers assume that `IEnumerable` is the best type to return a collection from a method. It's not a bad choice, but it does mean that the caller cannot make any assumptions about the collection they have been given, If enumerating the return value will be an in-memory operation or a potentially expensive action. They also don't know if they can safely enumerate it more than once. So often the caller ends up doing a `.ToList()` or similar on the collection you passed, which is wasteful if it was already a `List` already. This is actually another case in which `IReadOnlyCollection` can be a good fit if you know your method is always going to return an `in-memory collection`. 142 | 143 | So It gives your caller the ability to access the count of items and iterate through as many times as they like. 144 | 145 | 146 | ❌ **Bad** With returning `IEnumerable`, We don't know the result is an in-memory operation or a potentially expensive action so It can be a risky operation with multiple enumeration. 147 | ```csharp 148 | public IEnumerable GetAllProducts() 149 | { 150 | // ... 151 | } 152 | ``` 153 | 154 | ✅ **Good** If we are sure, our result is a in-memory collection it is better we return a `IReadOnlyCollection` instead of `IEnumerable`. 155 | ```csharp 156 | public IReadOnlyCollection GetAllProducts() 157 | { 158 | // ... 159 | } 160 | ``` 161 | 162 | ## Don't use zero-length arrays 163 | When initializing a zero-length array unnecessary memory allocation have to be made. Using `Array.Empty` can increase readability and reduce memory consumption as the memory is shared across all invocations of the method. 164 | 165 | ❌ **Bad** Return zero-initialized array. 166 | ```csharp 167 | public int[] MyFunc(string input) 168 | { 169 | if (input == null) 170 | return new int[0]; 171 | } 172 | ``` 173 | 174 | ✅ **Good** Use `Array.Empty` to increase readability and less allocations. 175 | ```csharp 176 | public int[] MyFunc(string input) 177 | { 178 | if (input == null) 179 | return Array.Empty(); 180 | } 181 | ``` 182 | 183 | > 💡 Info: For every generic version of `Array.Empty` the instance and memory is shared. 184 | ```csharp 185 | ReferenceEquals(new int[0], new int[0]); // Is false 186 | ReferenceEquals(Array.Empty(), Array.Empty()); // Is true 187 | ReferenceEquals(Array.Empty(), Array.Empty()); // Is false 188 | ``` 189 | 190 | ## Specify capacity of collections 191 | Often times collections like [`List`](https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.list-1.-ctor?view=net-6.0#system-collections-generic-list-1-ctor(system-int32)) offer constructors, which give the option to pass in the expected capacity of the list. 192 | The `DefaultCapacity` of a list is **4**. If added another item it will grow by the factor of 2. As the underlying structure of a `List` is a conventional array and arrays can't shrink or grow in size, a new array has to be created with the new size plus the content of the old array. This takes times and needs to unnecessary allocations. This is especially important for hot paths. 193 | 194 | ❌ **Bad** No capacity is given so the internal array has to be resized often. 195 | ```csharp 196 | var numbers = new List(); 197 | for (var i = 0; i < 20_000; i++) 198 | numbers.Add(i); 199 | ``` 200 | 201 | ✅ **Good** Capacity is given and the internal array of the `List` has the correct size. 202 | ```csharp 203 | var numbers = new List(20_000); 204 | for (var i = 0; i < 20_000; i++) 205 | numbers.Add(i); 206 | ``` 207 | 208 | ### Benchmark 209 | Result of the given example above: 210 | ```csharp 211 | | Method | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | Alloc Ratio | 212 | |-------------------- |----------:|---------:|---------:|------:|--------:|--------:|--------:|--------:|----------:|------------:| 213 | | ListWithoutCapacity | 102.64 us | 1.999 us | 3.340 us | 1.00 | 0.00 | 41.6260 | 41.6260 | 41.6260 | 256.36 KB | 1.00 | 214 | | ListWithCapacity | 39.55 us | 0.789 us | 1.698 us | 0.38 | 0.02 | 18.8599 | - | - | 78.18 KB | 0.30 | 215 | ``` 216 | 217 | ## Use `HashSet` to avoid linear searches 218 | If a collection is used often times to check whether or not an item is present a [`HashSet`](https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1?view=net-6.0) might be a better option than a `List`. The initial cost to create a `HashSet` is more expensive than a `List` but set-based operations are faster. In this case this holds true even for [`Any`](https://docs.microsoft.com/en-us/dotnet/api/system.linq.enumerable.any?view=net-6.0), which is basically a linear search through the array. 219 | 220 | ❌ **Bad** If often checking for an item in a collection `Any` and `Contains` from `List` can be slow. 221 | ```csharp 222 | var userIds = Enumerable.Range(0, 1_000).ToList(); 223 | usersIds.Any(u => u == 999); // Basically goes through 999 elements 224 | userIds.Contains(999); // Faster than Any but still slower than HashSet 225 | ``` 226 | 227 | ✅ **Good** `HashSet` performs faster for set-based operations. 228 | ```csharp 229 | var userIds = Enumerable.Range(0, 1_000).ToHashSet(); 230 | userIds.Contains(999); 231 | ``` 232 | 233 | ### Benchmark 234 | ```csharp 235 | public class ListVsHashSet 236 | { 237 | private const int Count = 10_000; 238 | private readonly List _list = Enumerable.Range(0, Count).ToList(); 239 | private readonly HashSet _hashSet = Enumerable.Range(0, Count).ToHashSet(); 240 | 241 | [Benchmark(Baseline = true)] 242 | public bool ListContains() => _list.Contains(Count - 1); 243 | 244 | [Benchmark] 245 | public bool ListAny() => _list.Any(l => l == Count - 1); 246 | 247 | [Benchmark] 248 | public bool HashSetContains() => _hashSet.Contains(Count - 1); 249 | } 250 | ``` 251 | 252 | Results: 253 | ```csharp 254 | | Method | Mean | Error | StdDev | Ratio | RatioSD | 255 | |---------------- |--------------:|--------------:|--------------:|-------:|--------:| 256 | | ListContains | 1,279.002 ns | 10.5561 ns | 9.3577 ns | 1.000 | 0.00 | 257 | | ListAny | 92,892.521 ns | 1,848.0088 ns | 3,379.1886 ns | 75.344 | 2.48 | 258 | | HashSetContains | 5.019 ns | 0.1364 ns | 0.1401 ns | 0.004 | 0.00 | 259 | ``` 260 | 261 | ## Use `HashSet` for set based operations like finding the intersection of two collections 262 | When performing set based operations like finding the intersection or union of two collection `HashSet` can be superior to a `List`. 263 | Be aware that the API is a bit different with a `HashSet` as the `HashSet` is not pure and modifies the `HashSet` on which the operation is called on. 264 | 265 | ❌ **Bad** Using LINQ and `List` to find the intersection of two collectionss 266 | ```csharp 267 | var evenNumbers = Enumerable.Range(0, 2_000).Where(i => i % 2 == 0).ToList(); 268 | var dividableByThree = Enumerable.Range(0, 2_000).Where(i => i % 3 == 0).ToList(); 269 | 270 | var evenNumbersDividableByThree = evenNumbers.Intersect(dividableByThree).ToList(); 271 | ``` 272 | 273 | ✅ **Good** Using a `HashSet` to find the intersection of two collections. 274 | ```csharp 275 | var evenNumbers = Enumerable.Range(0, 2_000).Where(i => i % 2 == 0).ToHashSet(); 276 | var dividableByThree = Enumerable.Range(0, 2_000).Where(i => i % 3 == 0).ToHashSet(); 277 | 278 | var evenNumbersDividableByThree = new HashSet(evenNumbers); 279 | evenNumbersDividableByThree.IntersectWith(dividableByThree); 280 | ``` 281 | 282 | ### Benchmark 283 | Runtimes for the given example above: 284 | 285 | Result: 286 | ```csharp 287 | | Method | Mean | Error | StdDev | Ratio | 288 | |-------------------- |---------:|---------:|---------:|------:| 289 | | InterSectionList | 25.69 us | 0.416 us | 0.369 us | 1.00 | 290 | | InterSectionHashSet | 11.88 us | 0.108 us | 0.090 us | 0.46 | 291 | ``` 292 | 293 | ## Use `ArraySegment` for efficient slicing of arrays 294 | When slicing an array a new array is created. This is especially expensive when the array is large. `ArraySegment` is a struct that holds a reference to the original array and the start and end index of the slice. This is much more efficient than creating a new one. Be aware that the `ArraySegment` is a read-only view of the original array and any changes to the original array will be reflected in the `ArraySegment`. 295 | 296 | ❌ **Bad** We create a new array when taking a slice of the original array, leading to extra memory allocations. 297 | ```csharp 298 | 299 | public int[] Slice(int[] array, int offset, int count) 300 | { 301 | var result = new int[count]; 302 | Array.Copy(array, offset, result, 0, count); 303 | return result; 304 | } 305 | 306 | var originalArray = new int[] { 1, 2, 3, 4, 5 }; 307 | var slicedArray = Slice(originalArray, 1, 3); // Creates a new array { 2, 3, 4 } 308 | ``` 309 | 310 | ✅ **Good** Use `ArraySegment` to create a view of the original array without creating a new array, avoiding the extra memory allocation. 311 | ```csharp 312 | public ArraySegment Slice(int[] array, int offset, int count) 313 | { 314 | return new ArraySegment(array, offset, count); 315 | } 316 | 317 | var originalArray = new int[] { 1, 2, 3, 4, 5 }; 318 | var slicedArray = Slice(originalArray, 1, 3); // A view over { 2, 3, 4 } in the original array 319 | ``` 320 | -------------------------------------------------------------------------------- /docs/debugging.md: -------------------------------------------------------------------------------- 1 | # Debugging 2 | This chapter looks into features, which can make debugging an application easier. 3 | 4 | ## Using `DebuggerDisplayAttribute` 5 | The [`DebuggerDisplayAttribute`](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.debuggerdisplayattribute?view=net-6.0) determines how a class or structure is displayed in the debugger. This can save time if you have complex object, where you have a few key information, which are the most significant. 6 | 7 | ```csharp 8 | var steven = new Person("Steven", 31, Array.Empty()); 9 | Console.WriteLine(steven); 10 | 11 | [DebuggerDisplay("{Name} is {Age} years old with {PetNames.Count} pets.")] 12 | public record Person(string Name, int Age, IReadOnlyCollection PetNames); 13 | ``` 14 | 15 | In the debugger window or if you hover over the `steven` object the debugger will show: 16 | ```no-class 17 | Steven is 31 years old with 0 pets. 18 | ``` -------------------------------------------------------------------------------- /docs/dictionary.md: -------------------------------------------------------------------------------- 1 | # `Dictionary` 2 | This page will give some tips and tricks while handling the `Dictionary` type. 3 | 4 | ## Safe way of getting a key from a `Dictionary` 5 | To get an element from a dictionary safely, we should check beforehand whether or not that element exists otherwise, we get an exception. Often times people use the `ContainsKey` method followed by the indexer of the dictionary. The problem here is that we do two lookups, even though we only need one (hashtable lookup). Also people find the second option more readable and it shows the intent better. 6 | 7 | ❌ **Bad** Use `ContainsKey` in combination with the indexer. 8 | ```csharp 9 | Dictionary mapping = GetMapping(); 10 | if (mapping.ContainsKey("some-key")) 11 | { 12 | var value = mapping["some-key"]; 13 | DoSomethingWithValue(value); 14 | // ... 15 | } 16 | ``` 17 | 18 | ✅ **Good** Avoid two lookups and get the value directly. 19 | ```csharp 20 | Dictionary mapping = GetMapping(); 21 | if (mapping.TryGetValue("some-key", out var value) 22 | { 23 | DoSomethingWithValue(value); 24 | // ... 25 | } 26 | ``` 27 | 28 | ## Define the initial size when creating a `Dictionary` when known 29 | `Dictionary` works internally a bit similiar to a `List`. Once a certain threshold of elements is reached, the internal type holding the information has to be resized. Resizing in this context means, create a new object of that type, which is bigger and copy all entries from the old storage into the new one. This operation is costly. When you build a `Dictionary` and the amount of entries is known, it is a better option to provide that capacity for the `Dictionary`. 30 | 31 | ❌ **Bad** Don't define the capacity, if known. 32 | ```csharp 33 | var dictionary = new Dictionary(); 34 | foreach (var key in _keys) 35 | dictionary[key] = key; 36 | ``` 37 | 38 | ✅ **Good** Defining the expected capacity. 39 | ```csharp 40 | var dictionary = new Dictionary(NumberOfKeys); 41 | foreach (var key in _keys) 42 | dictionary[key] = key; 43 | ``` 44 | 45 | ### Benchmark 46 | ```csharp 47 | [MemoryDiagnoser()] 48 | public class Benchmark 49 | { 50 | private List _keys; 51 | 52 | [Params(5, 10, 100)] 53 | public int NumberOfKeys { get; set; } 54 | 55 | [GlobalSetup] 56 | public void Setup() 57 | { 58 | _keys = Enumerable.Range(0, NumberOfKeys).Select(n => n.ToString()).ToList(); 59 | } 60 | 61 | [Benchmark(Baseline = true)] 62 | public Dictionary NoSize() 63 | { 64 | var dictionary = new Dictionary(); 65 | foreach (var key in _keys) dictionary[key] = key; 66 | 67 | return dictionary; 68 | } 69 | 70 | [Benchmark] 71 | public Dictionary WithSize() 72 | { 73 | var dictionary = new Dictionary(NumberOfKeys); 74 | foreach (var key in _keys) dictionary[key] = key; 75 | 76 | return dictionary; 77 | } 78 | } 79 | ``` 80 | 81 | Result: 82 | ```csharp 83 | | Method | NumberOfKeys | Mean | Error | StdDev | Ratio | Gen0 | Allocated | Alloc Ratio | 84 | |--------- |------------- |------------:|----------:|---------:|------:|-------:|----------:|------------:| 85 | | NoSize | 5 | 150.09 ns | 1.953 ns | 1.731 ns | 1.00 | 0.2217 | 464 B | 1.00 | 86 | | WithSize | 5 | 95.13 ns | 0.974 ns | 0.863 ns | 0.63 | 0.1568 | 328 B | 0.71 | 87 | | | | | | | | | | | 88 | | NoSize | 10 | 270.06 ns | 1.353 ns | 1.200 ns | 1.00 | 0.4740 | 992 B | 1.00 | 89 | | WithSize | 10 | 161.27 ns | 1.288 ns | 1.205 ns | 0.60 | 0.2103 | 440 B | 0.44 | 90 | | | | | | | | | | | 91 | | NoSize | 100 | 2,538.13 ns | 10.519 ns | 9.325 ns | 1.00 | 4.8714 | 10192 B | 1.00 | 92 | | WithSize | 100 | 1,574.53 ns | 8.705 ns | 7.717 ns | 0.62 | 1.4935 | 3128 B | 0.31 | 93 | ``` 94 | 95 | ## Pass in `StringComparer` to `Dictionary` 96 | `Dictionary` allows to pass in a `StringComparer` as a constructor argument. This allows for scenarios where you don't care about the case when retrieving a key from the dictionary. It also elimnates all the `ToUpper` or `ToLower` calls. Bonus points as it is slight more performant. 97 | 98 | ❌ **Bad** Using `ToLower` or `ToUpper` to be case insensitive. 99 | ```csharp 100 | var myDictionary = new Dictionary() 101 | { 102 | { "foo", 1 }, 103 | { "bar", 2 } 104 | } 105 | 106 | _ = myDictionary.ContainsKey("Foo"); // false 107 | _ = myDictionary.ContainsKey("Foo".ToLower()); // true 108 | _ = myDictionary.ContainsKey("foo"); // true 109 | ``` 110 | 111 | ✅ **Good** Pass in a `StringComparer` to the constructor. 112 | ```csharp 113 | var myDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase) 114 | { 115 | { "foo", 1 }, 116 | { "bar", 2 } 117 | } 118 | 119 | _ = myDictionary.ContainsKey("Foo"); // true 120 | _ = myDictionary.ContainsKey("FOO"); // true 121 | _ = myDictionary.ContainsKey("foo"); // true 122 | ``` -------------------------------------------------------------------------------- /docs/efcore/asnotracking.md: -------------------------------------------------------------------------------- 1 | ## Don't track readonly entities 2 | When loading models from the database Entity Framework will create proxies of your object for change detection. If you have load objects, which will never get updated this is an unnecessary overhead in terms of performance but also allocations as those proxies have their own memory footprint. 3 | 4 | ❌ **Bad** Change detection proxies are created even though it is only meant for reading. 5 | ```csharp 6 | return await blogPosts.Where(b => b.IsPublished) 7 | .Include(b => b.Tags) 8 | .ToListAsync(); 9 | ``` 10 | 11 | ✅ **Good** Explicitly say that the entities are not tracked by EF. 12 | ```csharp 13 | return await blogPosts.Where(b => b.IsPublished) 14 | .Include(b => b.Tags) 15 | .AsNoTracking() 16 | .ToListAsync(); 17 | ``` 18 | 19 | ### Benchmark 20 | A detailed setup and benchmark (which is referred below) can be found on the official https://docs.microsoft.com/en-us/ef/core/performance/efficient-querying#tracking-no-tracking-and-identity-resolution. 21 | 22 | ```csharp 23 | | Method | NumBlogs | NumPostsPerBlog | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | 24 | |------------- |--------- |---------------- |-----------:|---------:|---------:|-----------:|------:|--------:|--------:|--------:|------:|----------:| 25 | | AsTracking | 10 | 20 | 1,414.7 us | 27.20 us | 45.44 us | 1,405.5 us | 1.00 | 0.00 | 60.5469 | 13.6719 | - | 380.11 KB | 26 | | AsNoTracking | 10 | 20 | 993.3 us | 24.04 us | 65.40 us | 966.2 us | 0.71 | 0.05 | 37.1094 | 6.8359 | - | 232.89 KB | 27 | 28 | ``` -------------------------------------------------------------------------------- /docs/efcore/detailed_logging.md: -------------------------------------------------------------------------------- 1 | ## Enable Detailed logging in debug builds 2 | You can enable detailed logging in debug builds by adding the `EnableDetailedErrors` method to the `DbContextOptionsBuilder` in the `ConfigureServices` method of the `Startup` class. 3 | 4 | ```csharp 5 | services.AddDbContext(options => 6 | { 7 | options.UseSqlServer(connectionString) 8 | #if DEBUG 9 | .EnableDetailedErrors() 10 | #endif 11 | ; 12 | }); 13 | 14 | ``` 15 | 16 | This can give you vital insights, for example if you misconfigured some mappings/configurations. 17 | 18 | !!! warning 19 | This is only recommended for development and debugging purposes. It is not recommended to use this in production. 20 | 21 | You can combine this with the `LogTo` method to log out the translated queries: 22 | ```csharp 23 | services.AddDbContext(options => 24 | { 25 | options.UseSqlServer(connectionString) 26 | #if DEBUG 27 | .EnableDetailedErrors() 28 | .LogTo(Console.WriteLine) 29 | #endif 30 | ; 31 | }); 32 | ``` -------------------------------------------------------------------------------- /docs/efcore/in_memory_sqlite.md: -------------------------------------------------------------------------------- 1 | ## In-Memory database with SQLite 2 | 3 | SQLite can be used as an in-memory database for your code. This brings big advantage in testing. The database is transient, that means as soon as the connection gets closed the memory is freed. One downside is that the in-memory database is not thread-safe by default. This is achieved with the special `:memory:` data source. 4 | The advantage over the In-Memory database package provided via `Microsoft.EntityFrameworkCore.InMemory` that the SQLite version behaves closer to a real rational database. Also [Microsoft disencourages](https://docs.microsoft.com/en-us/ef/core/testing/testing-without-the-database#in-memory-provider) the use of the `InMemory` provider. 5 | 6 | ```csharp 7 | var connection = new SqliteConnection("DataSource=:memory:"); 8 | connection.Open(); 9 | 10 | services.AddDbContext(options => 11 | { 12 | options.UseSqlite(connection); 13 | }); 14 | ``` 15 | 16 | To make it work with multiple connections at a time, we can utilize the `cache=shared` identifier for the data source. More information can be found on the [official website](https://www.sqlite.org/inmemorydb.html#sharedmemdb). 17 | 18 | ```csharp 19 | var connection = new SqliteConnection("DataSource=myshareddb;mode=memory;cache=shared"); 20 | connection.Open(); 21 | 22 | services.AddDbContext(options => 23 | { 24 | options.UseSqlite(connection); 25 | }); 26 | ``` 27 | 28 | The database gets cleaned up when there is no active connection anymore. 29 | 30 | > 💡 Info: You have to install the [`Microsoft.EntityFrameworkCore.Sqlite`](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.Sqlite/) package to use the `UseSqlite` method. -------------------------------------------------------------------------------- /docs/efcore/index.md: -------------------------------------------------------------------------------- 1 | # EF Core 2 | This section will have more insights around Entity Framework Core related topics. -------------------------------------------------------------------------------- /docs/efcore/loadasync.md: -------------------------------------------------------------------------------- 1 | ## `LoadAsync` - Split queries into smaller chunks 2 | 3 | `LoadAsync` in combination with a EF-Core `DbContext` can load related entities. That is useful when handling with big data sets, cartiasian explosion (wanted or unwanted), lots of joins or unions. Big data sets for the reason, that it can happen that the timeout will be reached easily. As with `LoadAsync` the query is split up into smaller chunks, those might not hit the timeout individually. 4 | 5 | This code: 6 | ```csharp 7 | return await _context.Books 8 | .Include(b => b.BookCategories) 9 | .ThenInclude(c => c.Category) 10 | .Include(c => x.Author.Biography) 11 | .ToListAsync(); 12 | ``` 13 | 14 | Will be roughly translated to the following SQL statement: 15 | ```sql 16 | SELECT [b].[Id], [b].[AuthorId], [b].[Title], [a].[Id], [a0].[Id], [t].[BookId], [t].[CategoryId], [t].[Id], [t].[CategoryName], [a].[FirstName], [a].[LastName], [a0].[AuthorId], [a0].[Biography], [a0].[DateOfBirth], [a0].[Nationality], [a0].[PlaceOfBirth] 17 | FROM [Books] AS [b] 18 | INNER JOIN [Authors] AS [a] ON [b].[AuthorId] = [a].[Id] 19 | LEFT JOIN [AuthorBiographies] AS [a0] ON [a].[Id] = [a0].[AuthorId] 20 | LEFT JOIN ( 21 | SELECT [b0].[BookId], [b0].[CategoryId], [c].[Id], [c].[CategoryName] 22 | FROM [BookCategories] AS [b0] 23 | INNER JOIN [Categories] AS [c] ON [b0].[CategoryId] = [c].[Id] 24 | ) AS [t] ON [b].[Id] = [t].[BookId] 25 | ORDER BY [b].[Id], [a].[Id], [a0].[Id], [t].[BookId], [t].[CategoryId] 26 | ``` 27 | 28 | With `LoadAsync`: 29 | ```csharp 30 | var query = Context.Books; 31 | await query.Include(x => x.BookCategories) 32 | .ThenInclude(x => x.Category).LoadAsync(); 33 | await query.Include(x => x.Author).LoadAsync(); 34 | await query.Include(x => x.Author.Biography).LoadAsync(); 35 | return await query.ToListAsync(); 36 | ``` 37 | 38 | Which will be translated into: 39 | 40 | ```sql 41 | SELECT [b].[Id], [b].[AuthorId], [b].[Title], [t].[BookId], [t].[CategoryId], [t].[Id], [t].[CategoryName] 42 | FROM [Books] AS [b] 43 | LEFT JOIN ( 44 | SELECT [b0].[BookId], [b0].[CategoryId], [c].[Id], [c].[CategoryName] 45 | FROM [BookCategories] AS [b0] 46 | INNER JOIN [Categories] AS [c] ON [b0].[CategoryId] = [c].[Id] 47 | ) AS [t] ON [b].[Id] = [t].[BookId] 48 | ORDER BY [b].[Id], [t].[BookId], [t].[CategoryId] 49 | 50 | 51 | SELECT [b].[Id], [b].[AuthorId], [b].[Title], [a].[Id], [a].[FirstName], [a].[LastName] 52 | FROM [Books] AS [b] 53 | INNER JOIN [Authors] AS [a] ON [b].[AuthorId] = [a].[Id] 54 | 55 | SELECT [b].[Id], [b].[AuthorId], [b].[Title], [a].[Id], [a].[FirstName], [a].[LastName], [a0].[Id], [a0].[AuthorId], [a0].[Biography], [a0].[DateOfBirth], [a0].[Nationality], [a0].[PlaceOfBirth] 56 | FROM [Books] AS [b] 57 | INNER JOIN [Authors] AS [a] ON [b].[AuthorId] = [a].[Id] 58 | LEFT JOIN [AuthorBiographies] AS [a0] ON [a].[Id] = [a0].[AuthorId] 59 | 60 | SELECT [b].[Id], [b].[AuthorId], [b].[Title] 61 | FROM [Books] AS [b] 62 | ``` -------------------------------------------------------------------------------- /docs/efcore/maxlength_strings.md: -------------------------------------------------------------------------------- 1 | ## Define maximum length for strings 2 | When creating a SQL database via code first it is important to tell EF Core how long a string can be otherwise it will always be translated to `NVARCHAR(MAX)`. This has [performance implications](https://hungdoan.com/2017/04/13/nvarcharn-vs-nvarcharmax-performance-in-ms-sql-server/) as well as other problems like not being able to create an index on that column. Also a rogue application could flood the database. 3 | 4 | Having this model: 5 | ```csharp 6 | public class BlogPost 7 | { 8 | public int Id { get; private set; } 9 | public string Title { get; private set; } 10 | public string Content { get; private set; } 11 | } 12 | ``` 13 | 14 | ❌ **Bad** Not defining the maximum length of a string will lead to `NVARCHAR(max)`. 15 | ```csharp 16 | public class BlogPostConfiguration : IEntityTypeConfiguration 17 | { 18 | public void Configure(EntityTypeBuilder builder) 19 | { 20 | builder.HasKey(c => c.Id); 21 | builder.Property(c => c.Id).ValueGeneratedOnAdd(); 22 | } 23 | } 24 | ``` 25 | 26 | Will lead to generation of this SQL table: 27 | ```sql 28 | CREATE TABLE BlogPosts 29 | ( 30 | [Id] [int] NOT NULL, 31 | [Title] [NVARCHAR](MAX) NULL, 32 | [Content] [NVARCHAR](MAX) NULL 33 | ) 34 | ``` 35 | 36 | ✅ **Good** Defining the maximum length will reflect also in the database table. 37 | ```csharp 38 | public class BlogPostConfiguration : IEntityTypeConfiguration 39 | { 40 | public void Configure(EntityTypeBuilder builder) 41 | { 42 | builder.HasKey(c => c.Id); 43 | builder.Property(c => c.Id).ValueGeneratedOnAdd(); 44 | 45 | // Set title max length explicitly to 256 46 | builder.Property(c => c.Title).HasMaxLength(256); 47 | } 48 | } 49 | ``` 50 | 51 | Will lead to generation of this SQL table: 52 | ```sql 53 | CREATE TABLE BlogPosts 54 | ( 55 | [Id] [int] NOT NULL, 56 | [Title] [NVARCHAR](256) NULL, -- Now it only can hold 256 characters 57 | [Content] [NVARCHAR](MAX) NULL 58 | ) 59 | ``` -------------------------------------------------------------------------------- /docs/efcore/retry.md: -------------------------------------------------------------------------------- 1 | ## Retry on failure 2 | Entity Framework Core supports automatic retry on failure when saving changes to the database. This can be useful when working with a database that is subject to transient failures, such as a database on Azure SQL Database. The examples uses SQL Server but other providers, like PostgreSQL, also support this feature. 3 | 4 | ```csharp 5 | public void ConfigureServices(IServiceCollection services) 6 | { 7 | services.AddDbContext(options => 8 | options.UseSqlServer("your_connection_string", 9 | sqlServerOptions => 10 | { 11 | sqlServerOptions.EnableRetryOnFailure( 12 | maxRetryCount: 3, 13 | maxRetryDelay: TimeSpan.FromSeconds(5), 14 | errorNumbersToAdd: null); 15 | })); 16 | } 17 | ``` -------------------------------------------------------------------------------- /docs/efcore/splitquery.md: -------------------------------------------------------------------------------- 1 | ## Split multiple queries - `SplitQuery` 2 | 3 | The basic idea is to avoid "cartesian explosion". A cartesian explosion is when performing a JOIN on the one-to-many relationship then the rows of the one-side are being replicated N times (N = amount of rows on the many side). 4 | 5 | With SplitQuery instead of having 1 query, you will now have 2 queries where first the "one" side is loaded and in a separate query the "many" part is loaded. Where the SplitQuery can bring improvement it also has some major drawbacks. 6 | 7 | 1. You go two times to your database instead of once. 8 | 2. From the database point of view these are two separate queries. So no guarantee of data consistency. There could be race conditions interfering with your result set. 9 | 10 | 11 | ❌ **Bad** Every include is resolved by a `LEFT JOIN` leading to duplicated entries. 12 | ```csharp 13 | var blogPosts = await DbContext.Blog 14 | .Include(b => b.Posts) 15 | .Include(b => b.Tags) 16 | .ToListAsync(); 17 | ``` 18 | 19 | ✅ **Good** Get all related children by a separate query which gets resolved by an `INNER JOIN`. 20 | ```csharp 21 | var blogPosts = await DbContext.Blog 22 | .Include(b => b.Posts) 23 | .Include(b => b.Tags) 24 | .AsSplitQuery(); 25 | .ToListAsync(); 26 | ``` 27 | 28 | > 💡 Info: There are database which support multiple result sets in one query (for example SQL Server with **M** ultiple **A** ctive **R** esult **S** et). Here the performance is even better. For more information checkout the official Microsoft page about [`SplitQuery`](https://docs.microsoft.com/en-us/ef/core/querying/single-split-queries). 29 | -------------------------------------------------------------------------------- /docs/exceptions.md: -------------------------------------------------------------------------------- 1 | # Exceptions 2 | This chapter looks closer to exceptions and how exceptions are handled. 3 | 4 | ## Re-throwing exceptions incorrectly 5 | If done the wrong way, the stack trace including valuable information is gone. 6 | 7 | ❌ **Bad** Throwing the original object again will create a new stack trace 8 | ```csharp 9 | try 10 | { 11 | // logic which can throw here 12 | } 13 | catch (Exception exc) 14 | { 15 | throw exc; 16 | } 17 | ``` 18 | 19 | ⚠️ **Warning** Creating a new exception object with the original as `InnerException` but a new stack trace which can make debugging harder. 20 | ```csharp 21 | try 22 | { 23 | // logic which can throw here 24 | } 25 | catch (Exception exc) 26 | { 27 | throw new Exception("A message here", exc); 28 | } 29 | ``` 30 | 31 | ✅ **Good** Simply `throw` the exception to preserve the original stack trace. 32 | ```csharp 33 | try 34 | { 35 | // logic which can throw here 36 | } 37 | catch (Exception exc) 38 | { 39 | // Logic like logging here 40 | throw; 41 | } 42 | ``` 43 | 44 | > 💡 Info: Sometimes hiding the original stack trace might be the goal due to for example security related reasons. In the average case re-throwing via `throw;` is preferred. 45 | 46 | ## Pokemon Exception Handling 47 | Indiscriminately catching exceptions. "Pokemon - gotta catch 'em all" 48 | 49 | ❌ **Bad** Catching every exception and gracefully swallowing it. 50 | ```csharp 51 | try 52 | { 53 | MyOperation(); 54 | } 55 | catch (Exception exc) 56 | { 57 | // Do nothing 58 | } 59 | ``` 60 | 61 | Also: 62 | ```csharp 63 | try 64 | { 65 | MyOperation(); 66 | } 67 | catch 68 | { 69 | // Do nothing 70 | } 71 | ``` 72 | 73 | ✅ **Good** Catch the specific exception one would expect and handle it. 74 | ```csharp 75 | try 76 | { 77 | MyOperation(); 78 | } 79 | catch (InvalidOperationException exc) 80 | { 81 | logger.Log(exc); 82 | // More logic here and if wished re-throw the exception 83 | throw; 84 | } 85 | ``` 86 | 87 | ## Throwing `NullReferenceException`, `IndexOutOfRangeException`, and `AccessViolationException` 88 | This exceptions should not be thrown in public API's. The reasoning is that those exceptions should only be thrown by the runtime and normally indicate a bug in the software. 89 | For example one can avoid `NullReferenceException` by checking the object if it is `null` or not. On almost any case there different exceptions one can utilize, which have more semantic. 90 | 91 | ❌ **Bad** Throw `NullReferenceException` when checking a parameter. 92 | ```csharp 93 | public void DoSomething(string name) 94 | { 95 | if (name == null) 96 | { 97 | throw new NullReferenceException(); 98 | } 99 | ``` 100 | 101 | ✅ **Good** Indicating the argument is null and use the proper exception. 102 | ```csharp 103 | public void DoSomething(string name) 104 | { 105 | ArgumentNullException.ThrowIfNull(name); 106 | } 107 | ``` 108 | 109 | > 💡 Info: More details can be found [here.](https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/using-standard-exception-types) 110 | 111 | ## Don't throw `StackOverflowException` or `OutOfMemoryException` 112 | Both exceptions are meant only to be thrown from the runtime itself. Under normal circumstances recovering from a `StackOverflow` is hard to impossible. Therefore catching a `StackOverflowException` should also be avoided. 113 | And can catch `OutOfMemoryException` as they can also occur if a big array gets allocated but no more free space is available. That does not necessarily mean recovering from this is impossible. 114 | 115 | > 💡 Info: More details can be found [here.](https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/using-standard-exception-types) 116 | 117 | ## Exception filtering with `when` 118 | The `when` expression was introduced with C# 6 and allows a more readable way of filtering exceptions. 119 | 120 | ❌ **Bad** Less readable way. 121 | ```csharp 122 | try 123 | { 124 | await GetApiRequestAsync(); 125 | } 126 | catch (HttpRequestException e) 127 | { 128 | if (e.StatusCode == HttpStatusCode.BadRequest) 129 | { 130 | HandleBadRequest(e); 131 | } 132 | else if (e.StatusCode == HttpStatusCode.NotFound) 133 | { 134 | HandleNotFound(e); 135 | } 136 | } 137 | ``` 138 | 139 | ✅ **Good** More readable way. 140 | ```csharp 141 | try 142 | { 143 | await GetApiRequestAsync(); 144 | } 145 | catch (HttpRequestException e) when (e.StatusCode == HttpStatusCode.BadRequest) 146 | { 147 | HandleBadRequest(e); 148 | } 149 | catch (HttpRequestException e) when (e.StatusCode == HttpStatusCode.NotFound) 150 | { 151 | HandleNotFound(e); 152 | } 153 | ``` 154 | 155 | ## Finalizers should not throw exceptions 156 | Throwing an exception in a finalizer can lead to an immediate shutdown of the application without any cleanup. 157 | 158 | ❌ **Bad** Throwing an exception in the finalizer. 159 | ```csharp 160 | class MyClass 161 | { 162 | ~MyClass() 163 | { 164 | throw new NotImplementedException(); 165 | } 166 | } 167 | ``` 168 | 169 | ✅ **Good** Don't throw exceptions. 170 | ```csharp 171 | class MyClass 172 | { 173 | ~MyClass() 174 | { 175 | } 176 | } 177 | ``` 178 | 179 | > 💡 Info: More details can be found [here.](https://steven-giesel.com/blogPost/3b55d5ac-f62c-4b86-bfa3-62670f614761) 180 | 181 | ## Preserving the stack trace of an exception 182 | When catching an exception and re-throwing the exception via `throw exc` later the stack-trace gets altered, but sometimes capturing the exception can make sense. Since the .NET Framework 4.5 there is a small helper named [`ExceptionDispatchInfo`](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.exceptionservices.exceptiondispatchinfo?view=net-6.0), which let's you preserve that information. It offers a `Throw` method, which re-throws the saved exception with the given stack-trace. 183 | 184 | ❌ **Bad** Re-throwing the exception while losing the stack-trace 185 | ```csharp 186 | Exception? exception = null; 187 | 188 | try 189 | { 190 | Throws(); 191 | } 192 | catch (Exception exc) 193 | { 194 | exception = exc; 195 | } 196 | 197 | // Do something here ... 198 | 199 | if (exception is not null) 200 | throw exception; 201 | 202 | void Throws() => throw new Exception("💣"); 203 | ``` 204 | 205 | Produces the following output: 206 | ```no-class 207 | Unhandled exception. System.Exception: 💣 208 | at Program.
$(String[] args) in /Users/stgi/repos/Exception/Program.cs:line 15 209 | 210 | ``` 211 | 212 | ✅ **Good** Capturing the context to re-throw it later. 213 | ```csharp 214 | using System.Runtime.ExceptionServices; 215 | 216 | ExceptionDispatchInfo? exceptionDispatchInfo; 217 | 218 | try 219 | { 220 | Throws(); 221 | } 222 | catch (Exception exc) 223 | { 224 | exceptionDispatchInfo = ExceptionDispatchInfo.Capture(exc); 225 | } 226 | 227 | // Do something here ... 228 | 229 | if (exceptionDispatchInfo is not null) 230 | exceptionDispatchInfo.Throw(); 231 | 232 | void Throws() => throw new Exception("💣"); 233 | ``` 234 | 235 | Produces the following output: 236 | ```no-class 237 | Unhandled exception. System.Exception: 💣 238 | at Program.<
$>g__Throws|0_0() in /Users/stgi/repos/Exception/Program.cs:line 19 239 | at Program.
$(String[] args) in /Users/stgi/repos/Exception/Program.cs:line 7 240 | --- End of stack trace from previous location --- 241 | at Program.
$(String[] args) in /Users/stgi/repos/Exception/Program.cs:line 17 242 | ``` -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Tips and tricks 2 | A collection of tips, tricks and smaller pitfalls with smaller code snippets and explanation. 3 | 4 | Right now there are **#{entries}#** entries in the collection. 5 | The page has multiple sections divided by topic like `async`/`await` or `Blazor` and should give a good overview over common scenarios. 6 | 7 | Go ahead to the official page here: [tips and tricks](https://linkdotnet.github.io/tips-and-tricks). 8 | 9 | 10 | # Support & Contributing 11 | It helps out if you give this repository a ⭐. 12 | 13 | Feel free to contribute samples via [Pull Request](https://github.com/linkdotnet/tips-and-tricks/pulls). 14 | 15 | Thanks to all [contributors](https://github.com/linkdotnet/tips-and-tricks/graphs/contributors): 16 | 17 | 18 | 19 | 20 | 21 | # Contact 22 | If you have any questions, let me know. You can reach me here: 23 | 24 | 25 | [![Linkedin Badge](https://img.shields.io/badge/Steven%20Giesel-0077B5?style=flat&logo=linkedin&logoColor=white)](https://www.linkedin.com/in/steven-giesel/) 26 | [![Github Badge](https://img.shields.io/badge/linkdotnet-100000?style=flate&logo=github&logoColor=white)](https://github.com/linkdotnet/) 27 | [![Blog Badge](https://img.shields.io/badge/Steven%20Giesel-FFA500?style=flat&logo=rss&logoColor=white)](https://steven-giesel.com/) -------------------------------------------------------------------------------- /docs/linq.md: -------------------------------------------------------------------------------- 1 | # LINQ 2 | This chapter will handle topics about **LINQ**. 3 | 4 | ## Using `is` and `as` instead of `OfType` 5 | 6 | Using `is` and `as` cast should not be used in LINQ as it degrades readability and to some extend also performance (neglactable small under normal circumstances). 7 | 8 | ❌ **Bad** Less readable and unnecessary casts. 9 | ```csharp 10 | persons.Where(p => p is FullTimeEmployee).Select(p => p as FullTimeEmployee); 11 | persons.Where(p => p is FullTimeEmployee).Select(p => (FullTimeEmployee)p); 12 | persons.Select(p => p as FullTimeEmployee).Where(p => p != null); 13 | ``` 14 | 15 | ✅ **Good** Using `OfType` is cleaner and more readable. 16 | ```csharp 17 | persons.OfType(); 18 | ``` 19 | 20 | ## Using `Count()` instead of `All` or `Any` 21 | The problem with `Count()` is that LINQ has to enumerate every single entry in the enumeration to get the full count, where as `Any`/`All` would return immediately as soon as the condition is not satisfied anymore. 22 | In general conditions (excluding LINQ to SQL) `Any`/`All` in the worst case have the same runtime as `Count` but in best and average cases they are faster and more readable. 23 | 24 | ❌ **Bad** Less readable and unnecessary enumerations 25 | ```csharp 26 | persons.Count() > 0 27 | persons.Count(MyPredicate) > 0 28 | persons.Count(MyPredicate) == 0 29 | persons.Count(MyPredicate) == persons.Count() 30 | ``` 31 | 32 | ✅ **Good** More readable and avoids unnecessary enumerations 33 | ```csharp 34 | persons.Any() 35 | persons.Any(MyPredicate) 36 | !persons.Any(MyPredicate) 37 | persons.All(MyPredicate) 38 | ``` 39 | -------------------------------------------------------------------------------- /docs/logging/compiletime_logging.md: -------------------------------------------------------------------------------- 1 | ## Compie-time logging source generators 2 | 3 | .NET 6 introduced a new of defining how to log messages with source generators. 4 | 5 | ```csharp 6 | // The class has to be partial so we can extend the functionality 7 | public partial class MyService 8 | { 9 | private ILogger logger; 10 | 11 | public MyService(ILogger logger) => this.logger = logger; 12 | 13 | private void SomeActivity() 14 | { 15 | LogSomeActivity("Steven", "Giesel"); 16 | } 17 | 18 | // The source generator will automatically find the "logger" private field and use it 19 | [LoggerMessage(Message = "Hello {lastName}, {firstName}", Level = LogLevel.Information)] 20 | private partial void LogSomeActivity(string firstName, string lastName); 21 | } 22 | ``` 23 | 24 | It is also possible to have a static version, where the user passes in the `ILogger`. 25 | ```csharp 26 | public partial class MyService 27 | { 28 | private ILogger logger; 29 | 30 | public MyService(ILogger logger) => this.logger = logger; 31 | 32 | 33 | // The source generator will automatically find the "logger" private field and use it 34 | [LoggerMessage(Message = "Hello {lastName}, {firstName}", Level = LogLevel.Information)] 35 | public static partial void LogSomeActivity(ILogger logger, string firstName, string lastName); 36 | } 37 | ``` 38 | 39 | > 💡 Info: A detailed explanation can be found [here](https://steven-giesel.com/blogPost/48697958-4aee-474a-8920-e266d1d7b8fa). -------------------------------------------------------------------------------- /docs/logging/index.md: -------------------------------------------------------------------------------- 1 | # Logging 2 | This part will have a closer look about tips, tricks and pitfalls in regards to (structured) logging. -------------------------------------------------------------------------------- /docs/logging/string_interpolation.md: -------------------------------------------------------------------------------- 1 | ## Don't use string interpolation when logging 2 | String interpolation normally makes a string more readable but it interferes with structured logging. 3 | The reason is that string interpolation gets serialized ahead of time. The underlying logger just sees "one" string instead of its individual components. 4 | With that you will loose the ability to search for the format values. 5 | 6 | ❌ **Bad** String interpolation when calling logger methods. 7 | ```csharp 8 | User user = GetUserFromAPI(); 9 | DateTime when = DateTime.UtcNow; 10 | 11 | _logger.LogInformation($"Creating user: {user} at {when}"); 12 | ``` 13 | 14 | ✅ **Good** Use structured logging to preserve format information. 15 | ```csharp 16 | User user = GetUserFromAPI(); 17 | DateTime when = DateTime.UtcNow; 18 | 19 | _logger.LogInformation("Creating user: {User} at {When}", user, when); 20 | ``` -------------------------------------------------------------------------------- /docs/misc.md: -------------------------------------------------------------------------------- 1 | # Misc 2 | A collection of different topics which didn't fit in the other categories. 3 | 4 | ## `new Random` in a loop 5 | > 💡 Info: This part is only valid for the old .NET Framework and not the newer .NET Core / .NET 5 or 6 versions. 6 | 7 | In the .NET Framework creating the [`Random`](https://docs.microsoft.com/en-us/dotnet/api/system.random?view=net-6.0) class without any parameter in very short time spans will create identical outputs. 8 | Especially in for loops this can result in the same number over and over again. 9 | If no seed is provided for the `Random` class, it will create a seed which is time dependent. The resolution of the timer is depending on the implementation of the underlying OS. On most windows system the resolution is 15ms. 10 | 11 | ❌ **Bad** Will produce the same number 3 times. 12 | ```csharp 13 | for(var i = 0; i < 3; i++) 14 | { 15 | var random = new Random(); 16 | var number = random.Next(6); 17 | Console.WriteLine(number); 18 | } 19 | ``` 20 | 21 | Prints: 22 | > 4 23 | > 4 24 | > 4 25 | 26 | ✅ **Good** Extract the creation of the `Random` class outside the loop. 27 | ``` 28 | var random = new Random(); 29 | 30 | for(var i = 0; i < 3; i++) 31 | { 32 | var number = random.Next(6); 33 | Console.WriteLine(number); 34 | } 35 | ``` 36 | 37 | Prints: 38 | > 5 39 | > 3 40 | > 5 41 | 42 | ## Implementing `GetHashCode` 43 | `GetHashCode` is mainly used for `Dictionary`. Mainly two things are important to say an object (at least from an dictionary point of view) is equal: 44 | * `GetHashCode` for two objects are the same **and:** 45 | * `Equals` is the same. 46 | 47 | Dotnet does the first because its cheaper to calculate hashes and only if they match to check if the object is really the same. 48 | 49 | ❌ **Bad** GetHashCode which can cause collision 50 | ```csharp 51 | public class Point 52 | { 53 | public int X { get; set; } 54 | public int Y { get; set; } 55 | 56 | public override int GetHashCode() => X + Y; // Can lead to massive collisions 57 | } 58 | ``` 59 | 60 | ✅ **Good** Since .netcore there is a helper class called: [`HashCode.Combine`](https://docs.microsoft.com/en-us/dotnet/api/system.hashcode.combine?view=net-6.0). 61 | ```csharp 62 | public class Point 63 | { 64 | public int X { get; set; } 65 | public int Y { get; set; } 66 | 67 | public override int GetHashCode() => HashCode.Combine(X, Y); 68 | } 69 | ``` 70 | 71 | ✅ **Good** Also `ValueTuple` can be used for that purpose (since C# 7). 72 | ```csharp 73 | public class Point 74 | { 75 | public int X { get; set; } 76 | public int Y { get; set; } 77 | 78 | public override int GetHashCode() => (X, Y).GetHashCode(); 79 | } 80 | ``` 81 | 82 | ## Virtual member calls in constructor 83 | Calling a virtual member in a constructor can lead to unexpected behavior. The reason is that initializers run from most derived to base type but constructors run from base constructor to most derived. 84 | 85 | ❌ **Bad** This example will lead to a `NullReferenceException` 86 | ```csharp 87 | public class Parent 88 | { 89 | public Parent() { Console.WriteLine(GetType().Name); PrintHello(); } 90 | public virtual void PrintHello() {} 91 | } 92 | 93 | public class Child : Parent 94 | { 95 | private string foo; 96 | public Child() 97 | { 98 | foo = "Something"; 99 | } 100 | 101 | public override void PrintHello() 102 | { 103 | // foo will be null as it gets called in the base constructor before "our" 104 | // Child constructor is called and initializes the state 105 | Console.WriteLine(foo.ToUpperInvariant()); 106 | } 107 | } 108 | ``` 109 | Output: 110 | > Child 111 | Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object. 112 | 113 | ✅ **Good** Avoid virtual member calls via sealing the class or don't make the method virtual. 114 | ```csharp 115 | public class Parent 116 | { 117 | public Parent() { Console.WriteLine(GetType().Name); PrintHello(); } 118 | public void PrintHello() {} 119 | } 120 | 121 | public class Child : Parent 122 | { 123 | private string foo; 124 | public Child() 125 | { 126 | 127 | foo = "Something"; 128 | } 129 | } 130 | ``` 131 | 132 | > 💡 Info: It is perfectly fine to call a virtual member if the caller is the most derived type in the chain and there can't be another derived type (therefore the class is `sealed`). 133 | 134 | ## Don't have empty finalizers 135 | An empty finalizer does not offer any functionality and also decreases performance. Objects have to live at least one more generation before they can be removed. 136 | 137 | ❌ **Bad** Empty finalizer defined. 138 | ```csharp 139 | public class MyClass 140 | { 141 | ~MyClass() {} 142 | } 143 | ``` 144 | 145 | ✅ **Good** Don't define an empty finalizer. 146 | ```csharp 147 | public class MyClass 148 | { 149 | 150 | } 151 | ``` 152 | > 💡 Info: More information can be found [here](https://steven-giesel.com/blogPost/3b55d5ac-f62c-4b86-bfa3-62670f614761). 153 | 154 | ### Benchmark 155 | Results for the given example above: 156 | ```csharp 157 | | Method | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Allocated | 158 | |------------------- |-----------:|----------:|----------:|------:|--------:|-------:|-------:|----------:| 159 | | WithoutFinalizer | 2.205 ns | 0.1230 ns | 0.1416 ns | 1.00 | 0.00 | 0.0057 | - | 24 B | 160 | | WithEmptyFinalizer | 144.038 ns | 2.9594 ns | 6.1773 ns | 65.32 | 4.19 | 0.0057 | 0.0029 | 24 B | 161 | ``` 162 | 163 | ## `Enum.TryParse` unexpected behavior 164 | 165 | [`Enum.TryParse`](https://docs.microsoft.com/en-us/dotnet/api/system.enum.tryparse?view=net-6.0) parses a string and tries to convert it into a valid representation of a given enum. 166 | If the operation is successful it returns `true`, otherwise `false`. Have a look at the following code. What will be the output of that code? 167 | 168 | ```csharp 169 | var couldParse = Enum.TryParse("3", out WeekendDay weekendDay); 170 | 171 | Console.WriteLine($"Could parse: {couldParse}"); 172 | Console.WriteLine($"Value: {weekendDay}"); 173 | 174 | public enum WeekendDay 175 | { 176 | Saturday = 0, 177 | Sunday = 1, 178 | } 179 | ``` 180 | 181 | `3` is not a valid `WeekendDay` therefore `false` sure thing. But unfortunately no. The output is 182 | > Could parse: true 183 | > Value: 3 184 | 185 | You can try this on out [sharplab.io](https://sharplab.io/#v2:EYLgtghglgdgNAFxAJwK7wCYgNQB8ACATAIwCwAUBQG4TIAEAxgPaoA2GACrQM4CmdAXjoBRGKjAA6ACrIAnl2R8AFACIAzCrh0WCOgHVevANa8YGACIRZdAO6GTZy7ICUAbgoV8xAJxKAJCoAwizsdAAOPLwgdADezGyckQC+Km6ePv4qAGoQrKhRsXbGphZWKWmU5PhqdKbi+vYlThQxFHTtdADKEAioyBhWgnQADHBtHZ3oA9ZCxGPkSUA===) if you want. 186 | 187 | ✅ **Good** Use `Enum.IsDefined` to check if a given value is in the correct range. 188 | With [`Enum.IsDefined`](https://docs.microsoft.com/en-us/dotnet/api/system.enum.isdefined?view=net-6.0) we can check if the value is correct for a given enum. 189 | 190 | ```csharp 191 | var couldParse = Enum.TryParse("3", out WeekendDay weekendDay); 192 | if (couldParse && Enum.IsDefined(typeof(WeekendDay), weekendDay)) 193 | { 194 | // Only here we have a valid value for WeekendDay 195 | } 196 | ``` 197 | 198 | ## Be careful of closures 199 | Closures are "attached" to their parent to close over a certain variable. Basically the anonymous function / lambda is taking an argument outside of his own scope. 200 | The following code demonstrates such behavior: 201 | ```csharp 202 | var list = new List(); 203 | 204 | for(var i = 0; i < 5; i++) 205 | list.Add(() => Console.WriteLine(i)); 206 | 207 | list.ForEach(action => action()); 208 | ``` 209 | 210 | And here is the pitfall. The snippet might print something unexpecting: 211 | ```no-class 212 | 5 213 | 5 214 | 5 215 | 5 216 | 5 217 | ``` 218 | The reason is that behind the scenes the closures creates an anonymous class, which holds a reference to `i` (instead if a copy). That will lead to that all `Action`s point to the same reference of `i`, which after the for loop is `5`. The way to fix this is the following: 219 | 220 | ```csharp 221 | var list = new List(); 222 | 223 | for (var i = 0; i < 5; i++) 224 | { 225 | var t = i; 226 | list.Add(() => Console.WriteLine(t)); 227 | } 228 | 229 | list.ForEach(action => action()); 230 | ``` 231 | 232 | This works because copying an interger to a temporary variable will result in a new reference behind the scenes. You can fiddle around with that example on [sharplab.io](https://sharplab.io/#v2:D4AQDABCCMAsDcBYAUCATNFKBuBDAThADYCWAzgC4QC8EAdgKYDuEAMuRQDwxoB8AFAEokyFADMA9oX55CJGhDDwI8zhACsykgGptglAG8UEE8Q4A6AIIATa/yE1eUaAE5+JQcJQBfLMlKU5gBiUgCiuADGABb8kRQkEnSOEHEJdELCQA===). 233 | 234 | Additionally adding the `static` keyword to an anonymous function will prohibit any closures. So `list.Add(() => Console.WriteLine(i));` will result in an compiler error. -------------------------------------------------------------------------------- /docs/nuget/floating_versioning.md: -------------------------------------------------------------------------------- 1 | ## Floating version 2 | NuGet gives the option to use wildcards instead of concrete versions. This gives the ability to use always the latest (major/minor/patch) version of a certain package. 3 | 4 | ```xml 5 | 6 | 7 | 8 | 9 | 10 | 11 | ``` 12 | 13 | This can be especially helpful when working with preview version of dotnet itself. If you always want to have the latest preview version of let's say the OpenIdConnect-package and Entity Framework core, you could do the following: 14 | ```xml 15 | 16 | 17 | 18 | 19 | ``` -------------------------------------------------------------------------------- /docs/nuget/index.md: -------------------------------------------------------------------------------- 1 | # NuGet 2 | 3 | This section will look into tips and tricks when working with NuGet. -------------------------------------------------------------------------------- /docs/regex/index.md: -------------------------------------------------------------------------------- 1 | # Regular Expressions 2 | This chapter will look more into the usage of regular expressions. -------------------------------------------------------------------------------- /docs/regex/regex_options.md: -------------------------------------------------------------------------------- 1 | ## Use the correct `RegexOption` when defining the regular expression. 2 | 3 | When defining a regular expression, one can define [`RegexOption`](https://docs.microsoft.com/en-us/dotnet/api/system.text.regularexpressions.regexoptions?view=net-6.0)s, which enable fine-grained control over the regular expression engine. That also works with the newly introduced [`RegexGenerator`](https://devblogs.microsoft.com/dotnet/regular-expression-improvements-in-dotnet-7/) introduced in .net 7, as they use the same attributes. 4 | 5 | ### `Compiled` 6 | This flags (which is the default for `RegexGenerator` in .net7, so you do not have to provide that argument for them) enables the compilation of the regular expression into the MSIL code of the application. That brings performance improvements in trade-off in expense of a bigger startup time of your application. There are only small scenarios, where this flag does not make sense. 7 | 8 | ### `ExplicitCapture` 9 | If you don't work with capture groups, you can safely provide this flag as this tells the regex engine **not** to look for implicit captures. Explicit captures are something like this: `?`. 10 | 11 | ### Defining multiple values 12 | As `RegexOption` are defined as flags, you can provide multiple arguments delimited by the `|` characters (bitwise or). 13 | 14 | ```csharp 15 | public class RegexBenchmark 16 | { 17 | private const string Sentence = "Hello this is an example of stock tips!"; 18 | private const string RegexString = @"(\W|^)stock\stips(\W|$)"; 19 | private static readonly Regex StockTips = new(RegexString); 20 | private static readonly Regex StockTipsCompiled = new(RegexString, RegexOptions.Compiled); 21 | private static readonly Regex StockTipsExplicitCapture = new(RegexString, RegexOptions.ExplicitCapture); 22 | private static readonly Regex StockTipsAllCombined = new(RegexString, 23 | RegexOptions.Compiled | RegexOptions.ExplicitCapture); 24 | 25 | [Benchmark(Baseline = true)] 26 | public bool HasMatch() => StockTips.IsMatch(Sentence); 27 | 28 | [Benchmark] 29 | public bool HasMatchCompiled() => StockTipsCompiled.IsMatch(Sentence); 30 | 31 | [Benchmark] 32 | public bool HasMatchExplicitCapture() => StockTipsExplicitCapture.IsMatch(Sentence); 33 | 34 | [Benchmark] 35 | public bool HasMatchCombined() => StockTipsAllCombined.IsMatch(Sentence); 36 | } 37 | ``` 38 | 39 | Results: 40 | ```csharp 41 | | Method | Mean | Error | StdDev | Ratio | 42 | |------------------------ |-----------:|---------:|---------:|------:| 43 | | HasMatch | 1,348.4 ns | 17.48 ns | 16.35 ns | 1.00 | 44 | | HasMatchCompiled | 650.0 ns | 3.59 ns | 3.36 ns | 0.48 | 45 | | HasMatchExplicitCapture | 1,062.4 ns | 5.50 ns | 5.14 ns | 0.79 | 46 | | HasMatchCombined | 528.0 ns | 3.09 ns | 2.89 ns | 0.39 | 47 | ``` -------------------------------------------------------------------------------- /docs/regex/right_to_left.md: -------------------------------------------------------------------------------- 1 | ## Right to left search 2 | The [`RegexOption.RightToLeft`](https://docs.microsoft.com/en-us/dotnet/api/system.text.regularexpressions.regexoptions?view=net-6.0) allows the regex engine to search from the end to the start. That can bring major gains if you expect the match at the end of the string rather than the beginning. 3 | 4 | ```csharp 5 | public class RegexBenchmark 6 | { 7 | private const string Sentence = "Hello this is an example of stock tips!"; 8 | private const string RegexString = @"(\W|^)stock\stips(\W|$)"; 9 | private static readonly Regex StockTips = new(RegexString, RegexOptions.Compiled); 10 | private static readonly Regex StockTipsRightToLeft = new(RegexString, 11 | RegexOptions.Compiled | RegexOptions.RightToLeft); 12 | 13 | [Benchmark(Baseline = true)] 14 | public bool HasMatch() => StockTips.IsMatch(Sentence); 15 | 16 | [Benchmark] 17 | public bool HasMatchRightToLeft() => StockTipsRightToLeft.IsMatch(Sentence); 18 | } 19 | ``` 20 | 21 | Results: 22 | ```csharp 23 | | Method | Mean | Error | StdDev | Ratio | 24 | |-------------------- |----------:|---------:|---------:|------:| 25 | | HasMatch | 649.54 ns | 1.923 ns | 1.799 ns | 1.00 | 26 | | HasMatchRightToLeft | 48.23 ns | 0.153 ns | 0.143 ns | 0.07 | 27 | ``` -------------------------------------------------------------------------------- /docs/regex/source_generator.md: -------------------------------------------------------------------------------- 1 | # Source Code Generators 2 | This page will look at how source code generators can impact regular expressions. 3 | 4 | ## Source Code Generators and Regular Expressions 5 | Since .NET 7 we have the possibility to use source code generators for regular expressions. The advantage over the regular approach is that we can get the same performance as `new Regex("...", RegexOptions.Compiled)` and the startup benefit of `Regex.CompileToAssembly`, but without the complexity of `CompileToAssembly`. As the code is generated it can be viewed and debugged. 6 | 7 | So instead of this code: 8 | ```csharp 9 | private static readonly Regex HelloOrWorldCompiled = 10 | new("Hello|World", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); 11 | ``` 12 | 13 | We can write it like this: 14 | ```csharp 15 | [GeneratedRegex("Hello|World", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] 16 | private static partial Regex HelloOrWorldGenerator(); 17 | ``` -------------------------------------------------------------------------------- /docs/strings.md: -------------------------------------------------------------------------------- 1 | # strings 2 | This chapter shows some tips, tricks and pitfalls in combination with `string`'s. 3 | 4 | ## Use `StringBuilder` when concatenating a lot of strings 5 | As `string`'s are immutable in C# concatenating those will result in many allocations and loss of performance. In scenarios where many strings get concatenated a [`StringBuilder`](https://docs.microsoft.com/en-us/dotnet/api/system.text.stringbuilder?view=net-6.0) is preferred. The same applies to operations like `string.Join` or add a single character to a `string`. 6 | 7 | ❌ **Bad** Will use a lot of allocations and will result in a performance penalty. 8 | ```csharp 9 | var outputString = ""; 10 | 11 | for (var i = 0; i < 25; i++) 12 | { 13 | outputString += "test" + i; 14 | } 15 | ``` 16 | 17 | ✅ **Good** Usage of `StringBuilder` will reduce the allocations dramatically and also performs better. 18 | Here is a comparison of both methods: 19 | 20 | ``` 21 | | Method | Times | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen 0 | Allocated | 22 | |-------------------- |------ |------------:|----------:|------------:|------------:|------:|--------:|--------:|----------:| 23 | | StringConcat | 10 | 298.7 ns | 1.86 ns | 1.45 ns | 298.3 ns | 1.00 | 0.00 | 0.4549 | 2 KB | 24 | | StringBuilderAppend | 10 | 436.2 ns | 5.31 ns | 4.15 ns | 437.1 ns | 1.46 | 0.02 | 0.4206 | 2 KB | 25 | | | | | | | | | | | | 26 | | StringConcat | 100 | 15,025.7 ns | 739.33 ns | 2,011.40 ns | 14,579.0 ns | 1.00 | 0.00 | 39.5203 | 161 KB | 27 | | StringBuilderAppend | 100 | 5,989.5 ns | 415.73 ns | 1,225.78 ns | 6,416.1 ns | 0.41 | 0.12 | 3.9063 | 16 KB | 28 | ``` 29 | 30 | ## Getting the printable length of a string or character 31 | Retrieving the length of a string can often be done via `"my string".Length`, which in a lot of scenarios is good enough. Under the hood [`string.Length`](https://docs.microsoft.com/en-us/dotnet/api/system.string.length?view=net-6.0) will return the number of characters in this string object. Unfortunately that does not always map one to one with the printed characters on screen. 32 | 33 | ❌ **Bad** Assuming every printed characters has the same length. 34 | ```csharp 35 | Console.Write("The following string has the length of 1: "); 36 | Console.WriteLine("🏴󠁧󠁢󠁥󠁮󠁧󠁿".Length); 37 | ``` 38 | 39 | Output: 40 | > The following string has the length of 1: 14 41 | 42 | Emojis can consist out of "other" emojis making the length very variable. Also other charaters like the following are wider: 43 | ```csharp 44 | Console.WriteLine("𝖙𝖍𝖎𝖘".Length); // Prints 8 45 | ``` 46 | 47 | ✅ **Good** Take [`StringInfo.LengthInTextElements`](https://docs.microsoft.com/en-us/dotnet/api/system.globalization.stringinfo.lengthintextelements?view=net-6.0) to know the amount of printed characters. 48 | ```csharp 49 | Console.WriteLine(new StringInfo("🏴󠁧󠁢󠁥󠁮󠁧󠁿").LengthInTextElements); 50 | ``` 51 | 52 | Output: 53 | > 1 54 | 55 | To summarize: `string.Length` will give return the internal array size not the length of printed characters. `StringInfo.LengthInTextElements` will return the amount of printed characters. 56 | 57 | > 💡 Info: Some more information about Unicode, UTF-8, UTF-16 and UTF-32 can be found [here](https://medium.com/bobble-engineering/emojis-from-a-programmers-eye-ca65dc2acef0). 58 | 59 | ## Use `StringComparison` instead of `ToLowerCase` or `ToUpperCase` for insensitive comparison 60 | 61 | Lots of code is using `"ABC".ToLowerCase() == "abc".ToLowerCase()` to compare two strings, when casing doesn't matter. The problem with that code is `ToLowerCase` as well as `ToUpperCase` creates a new string instance, resulting in unnecessary allocations and performance loss. 62 | 63 | 64 | ❌ **Bad** Using new allocations for comparing strings. 65 | ```csharp 66 | var areStringsEqual = "abc".ToUpperCase() == "ABC".ToUpperCase(); 67 | ``` 68 | 69 | ✅ **Good** Use of the `string.Equals` overload with the appropriate `StringComparison` technique. 70 | ```csharp 71 | var areStringsEqual = string.Equals("ABC", "abc", StringComparison.OrdinalIgnoreCase); 72 | ``` 73 | 74 | ### Benchmark 75 | ```csharp 76 | [MemoryDiagnoser] 77 | [HideColumns(Column.Arguments)] 78 | public class StringBenchmark 79 | { 80 | [Benchmark(Baseline = true)] 81 | [Arguments( 82 | "HellO WoRLD, how are you? You are doing good?", 83 | "hElLO wOrLD, how Are you? you are doing good?")] 84 | public bool AreEqualToLower(string a, string b) => a.ToLower() == b.ToLower(); 85 | 86 | [Benchmark(Baseline = false)] 87 | [Arguments( 88 | "HellO WoRLD, how are you? You are doing good?", 89 | "hElLO wOrLD, how Are you? you are doing good?")] 90 | public bool AreEqualStringComparison(string a, string b) => string.Equals(a, b, StringComparison.OrdinalIgnoreCase); 91 | } 92 | ``` 93 | 94 | Results: 95 | ``` 96 | | Method | Mean | Error | StdDev | Ratio | Gen0 | Allocated | Alloc Ratio | 97 | |------------------------- |---------:|---------:|---------:|------:|-------:|----------:|------------:| 98 | | AreEqualToLower | 60.93 ns | 1.008 ns | 0.943 ns | 1.00 | 0.0356 | 224 B | 1.00 | 99 | | AreEqualStringComparison | 16.10 ns | 0.030 ns | 0.028 ns | 0.26 | - | - | 0.00 | 100 | ``` 101 | 102 | ## Prefer `StartsWith` over `IndexOf() == 0` 103 | The problem with IndexOf is, that it will go through the whole string in the worst case. StartsWith on the contrary will directly abort one the first mismatch. 104 | 105 | ❌ **Bad** Using `IndexOf` which might run through the whole string. 106 | ```csharp 107 | var startsWithHallo = "Hello World".IndexOf("Hallo") == 0; 108 | ``` 109 | 110 | ✅ **Good** More readable, as well as more performant with `StartsWith`. 111 | ```csharp 112 | var startsWithHallo = "Hello World".StartsWith("Hallo"); 113 | ``` 114 | 115 | ### Benchmark 116 | ```csharp 117 | [Benchmark(Baseline = true)] 118 | [Arguments("That is a sentence", "Thzt")] 119 | public bool IndexOf(string haystack, string needle) => haystack.IndexOf(needle, StringComparison.OrdinalIgnoreCase) == 0; 120 | 121 | [Benchmark] 122 | [Arguments("That is a sentence", "Thzt")] 123 | public bool StartsWith(string haystack, string needle) => 124 | haystack.StartsWith(needle, StringComparison.OrdinalIgnoreCase); 125 | ``` 126 | 127 | Results: 128 | ``` 129 | | Method | haystack | needle | Mean | Error | StdDev | Ratio | 130 | |----------- |------------------- |------- |----------:|----------:|----------:|------:| 131 | | IndexOf | That is a sentence | Thzt | 21.966 ns | 0.1584 ns | 0.1482 ns | 1.00 | 132 | | StartsWith | That is a sentence | Thzt | 3.066 ns | 0.0142 ns | 0.0126 ns | 0.14 | 133 | ``` 134 | 135 | ## Prefer `AsSpan` over `Substring` 136 | `Substring` always allocates a new string object on the heap. If you have a method that accepts a `Span` or `ReadOnlySpan` you can avoid these allocations. A prime example is `string.Concat` that takes a `ReadOnlySpan` as an input parameter. 137 | 138 | ❌ **Bad** Creating new `string` objects that are directly discarded afterward. 139 | ```csharp 140 | var output = Text.Substring(0, 5) + " - " + Text.Substring(11, 4); 141 | ``` 142 | 143 | ✅ **Good** Directly use the underlying memory to avoid heap allocations. 144 | ```csharp 145 | var output = string.Concat(Text.AsSpan(0, 5), " - ", Text.AsSpan(11, 4)); 146 | ``` 147 | 148 | ### Benchmark 149 | ```csharp 150 | [MemoryDiagnoser] 151 | public class Benchmark 152 | { 153 | [Params("Hello dear world")] 154 | public string Text { get; set; } 155 | 156 | [Benchmark] 157 | public string Substring() 158 | => Text.Substring(0, 5) + " - " + Text.Substring(11, 4); 159 | 160 | [Benchmark] 161 | public string AsSpanConcat() 162 | => string.Concat(Text.AsSpan(0, 5), " - ", Text.AsSpan(11, 4)); 163 | } 164 | ``` 165 | 166 | Results: 167 | ```no-class 168 | | Method | Text | Mean | Error | StdDev | Gen0 | Allocated | 169 | |------------- |----------------- |---------:|---------:|---------:|-------:|----------:| 170 | | Substring | Hello dear world | 21.18 ns | 0.085 ns | 0.076 ns | 0.0179 | 112 B | 171 | | AsSpanConcat | Hello dear world | 10.20 ns | 0.021 ns | 0.018 ns | 0.0076 | 48 B | 172 | ``` -------------------------------------------------------------------------------- /docs/valuetuple.md: -------------------------------------------------------------------------------- 1 | # ValueTuple 2 | This section looks into tips and tricks of the `ValueTuple` type which was introduced in C# 7. 3 | 4 | ## Easy `IEquatable` implementation 5 | `ValueTuple` can be leveraged to have an easy and readable implementation of [`IEquatable`](https://docs.microsoft.com/en-us/dotnet/api/system.iequatable-1?view=net-6.0). 6 | 7 | ```csharp 8 | public class Dimension : IEquatable 9 | { 10 | public Dimension(int width, int height) 11 | => (Width, Height) = (width, height); 12 | 13 | public int Width { get; } 14 | public int Height { get; } 15 | 16 | public bool Equals(Dimension other) 17 | => (Width, Height) == (other?.Width, other?.Height); 18 | 19 | public override bool Equals(object obj) 20 | => obj is Dimension dimension && Equals(dimension); 21 | 22 | public override int GetHashCode() 23 | => (Width, Height).GetHashCode(); 24 | } 25 | ``` 26 | 27 | ## Swap two values 28 | `ValueTuple` can be used to swap two (or more) variables without the usage of a temporary variable. 29 | 30 | ❌ **Bad** Using temporary variable. 31 | ```csharp 32 | int a = 10; 33 | int b = 15; 34 | 35 | var tmp = a; 36 | a = b; 37 | b = tmp; 38 | ``` 39 | 40 | ✅ **Good** Using `ValueTuple`. 41 | ```csharp 42 | int a = 10; 43 | int b = 15; 44 | 45 | (a, b) = (b, a); 46 | ``` -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Tips and tricks 2 | site_url: https://linkdotnet.github.io/tips-and-tricks/ 3 | site_description: A collection of tips and tricks with smaller code snippets and explanation. 4 | docs_dir: docs 5 | repo_name: linkdotnet/tips-and-tricks 6 | repo_url: https://github.com/linkdotnet/tips-and-tricks 7 | edit_uri: edit/main/docs/ 8 | theme: 9 | name: material 10 | font: 11 | text: Sora 12 | palette: 13 | - media: "(prefers-color-scheme: light)" 14 | scheme: default 15 | toggle: 16 | icon: material/toggle-switch-off-outline 17 | name: Switch to dark mode 18 | - media: "(prefers-color-scheme: dark)" 19 | scheme: slate 20 | toggle: 21 | icon: material/toggle-switch 22 | name: Switch to light mode 23 | prev_next_buttons_location: both 24 | navigation_depth: 4 25 | titles_only: False 26 | sticky_navigation: True 27 | markdown_extensions: 28 | - smarty 29 | - sane_lists 30 | - fenced_code 31 | - meta 32 | - admonition 33 | - attr_list 34 | - pymdownx.arithmatex 35 | - pymdownx.betterem: 36 | smart_enable: all 37 | - pymdownx.caret 38 | - pymdownx.critic 39 | - pymdownx.details 40 | - pymdownx.inlinehilite 41 | - pymdownx.magiclink 42 | - pymdownx.mark 43 | - pymdownx.smartsymbols 44 | - pymdownx.superfences 45 | - pymdownx.tasklist: 46 | custom_checkbox: true 47 | - pymdownx.tabbed 48 | - pymdownx.tilde 49 | - codehilite 50 | - footnotes 51 | - toc: 52 | permalink: true 53 | nav: 54 | - Array: array.md 55 | - Async / Await: async_await.md 56 | - Blazor: blazor.md 57 | - Collections: collections.md 58 | - Debugging: debugging.md 59 | - Dictionary: dictionary.md 60 | - Exceptions: exceptions.md 61 | - LINQ: linq.md 62 | - Misc: misc.md 63 | - Strings: strings.md 64 | - ValueTuple: valuetuple.md 65 | - Advanced: 66 | - advanced/index.md 67 | - ArrayPools to minimize allocations: advanced/arraypool.md 68 | - Boxing/Unboxing: advanced/boxing.md 69 | - Iterate through a List: advanced/iterate_list.md 70 | - Lambda vs method group: advanced/lambda_methodgroup.md 71 | - Lazy: advanced/lazy.md 72 | - SIMD: advanced/SIMD.md 73 | - Span: advanced/span.md 74 | - stackalloc: advanced/stackalloc.md 75 | - struct: advanced/struct.md 76 | - EF Core: 77 | - efcore/index.md 78 | - Detailed Logging: efcore/detailed_logging.md 79 | - Don't track readonly entities: efcore/asnotracking.md 80 | - Define maximum length for strings: efcore/maxlength_strings.md 81 | - LoadAsync: efcore/loadasync.md 82 | - In Memory database with SQLite: efcore/in_memory_sqlite.md 83 | - Retry on failure: efcore/retry.md 84 | - SplitQuery: efcore/splitquery.md 85 | - Logging: 86 | - logging/index.md 87 | - Compile-time logging source generator: logging/compiletime_logging.md 88 | - Don't use string interpolation: logging/string_interpolation.md 89 | - NuGet: 90 | - nuget/index.md 91 | - Use floating versioning: nuget/floating_versioning.md 92 | - Regular Expressions: 93 | - regex/index.md 94 | - RegExOptions: regex/regex_options.md 95 | - Right to left search: regex/right_to_left.md 96 | - Source Code Generators: regex/source_generator.md 97 | extra: 98 | generator: false 99 | social: 100 | - icon: fontawesome/brands/linkedin 101 | link: https://www.linkedin.com/in/steven-giesel/ 102 | - icon: fontawesome/brands/github 103 | link: https://github.com/linkdotnet 104 | - icon: fontawesome/solid/rss 105 | link: https://steven-giesel.com/ 106 | 107 | 108 | copyright: 2022 Steven Giesel. --------------------------------------------------------------------------------