@cratis/pi 7.101.0 → 7.103.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package/corpus/harnesses/copilot/instructions/github-actions.instructions.md +17 -0
- package/package/corpus/harnesses/copilot/instructions/profiles.instructions.md +10 -1
- package/package/corpus/harnesses/copilot/instructions/pull-requests.instructions.md +2 -0
- package/package/corpus/harnesses/copilot/instructions/secrets.instructions.md +4 -0
- package/package/corpus/hooks/README.md +11 -1
- package/package/corpus/hooks/scripts/cratis-check-pr.mjs +12 -1
- package/package/corpus/hooks/scripts/cratis-guard-secrets.sh +79 -23
- package/package/corpus/hooks/scripts/cratis-guard-writes.sh +145 -40
- package/package/corpus/rules/github-actions.md +17 -0
- package/package/corpus/rules/profiles.md +10 -1
- package/package/corpus/rules/pull-requests.md +2 -0
- package/package/corpus/rules/secrets.md +4 -0
- package/package/corpus/skills/analyzing-dotnet-performance/LICENSE +23 -0
- package/package/corpus/skills/analyzing-dotnet-performance/NOTICE +36 -0
- package/package/corpus/skills/analyzing-dotnet-performance/SKILL.md +214 -0
- package/package/corpus/skills/analyzing-dotnet-performance/references/async-patterns.md +122 -0
- package/package/corpus/skills/analyzing-dotnet-performance/references/collections-and-linq.md +220 -0
- package/package/corpus/skills/analyzing-dotnet-performance/references/critical-patterns.md +291 -0
- package/package/corpus/skills/analyzing-dotnet-performance/references/io-and-serialization.md +135 -0
- package/package/corpus/skills/analyzing-dotnet-performance/references/memory-and-strings.md +219 -0
- package/package/corpus/skills/analyzing-dotnet-performance/references/regex-patterns.md +95 -0
- package/package/corpus/skills/analyzing-dotnet-performance/references/structural-patterns.md +38 -0
- package/package/corpus/skills/binlog-generation/LICENSE +23 -0
- package/package/corpus/skills/binlog-generation/NOTICE +22 -0
- package/package/corpus/skills/binlog-generation/SKILL.md +129 -0
- package/package/corpus/skills/check-bin-obj-clash/LICENSE +23 -0
- package/package/corpus/skills/check-bin-obj-clash/NOTICE +27 -0
- package/package/corpus/skills/check-bin-obj-clash/SKILL.md +127 -0
- package/package/corpus/skills/check-bin-obj-clash/references/common-causes-and-fixes.md +206 -0
- package/package/corpus/skills/coverage-analysis/LICENSE +23 -0
- package/package/corpus/skills/coverage-analysis/NOTICE +32 -0
- package/package/corpus/skills/coverage-analysis/SKILL.md +198 -0
- package/package/corpus/skills/coverage-analysis/references/guidelines.md +68 -0
- package/package/corpus/skills/coverage-analysis/references/output-format.md +91 -0
- package/package/corpus/skills/coverage-analysis/references/report-generation.md +58 -0
- package/package/corpus/skills/coverage-analysis/references/setup-discovery.md +307 -0
- package/package/corpus/skills/coverage-analysis/scripts/Compute-CrapScores.ps1 +80 -0
- package/package/corpus/skills/coverage-analysis/scripts/CoverageData.ps1 +272 -0
- package/package/corpus/skills/coverage-analysis/scripts/Extract-MethodCoverage.ps1 +65 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/LICENSE +2 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/SKILL.md +201 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/references/build-once-and-shard.md +158 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/references/caching.md +86 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/references/flaky-and-hung-jobs.md +70 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/references/publish-pipelines.md +67 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/references/required-checks-and-path-filters.md +117 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/references/runner-sizing-and-images.md +65 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/references/security-and-pinning.md +73 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/references/sources.md +33 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/references/test-setup-cost.md +82 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/scripts/ci-timings.sh +356 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/scripts/fixtures/jobs/1003.json +79 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/scripts/fixtures/jobs/1007.json +79 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/scripts/fixtures/jobs/1010.json +79 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/scripts/fixtures/runs.json +102 -0
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/LICENSE +2 -0
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/SKILL.md +244 -0
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/docker-images.md +86 -0
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/dotnet-nuget.md +87 -0
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/go-jvm-python-elixir.md +83 -0
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/npm-yarn.md +78 -0
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/other-environments.md +60 -0
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/worked-examples.md +97 -0
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/scripts/cratis-local-packages.sh +778 -0
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/scripts/npm-restore-check.cjs +109 -0
- package/package/corpus/skills/csharp-refactoring/LICENSE +23 -0
- package/package/corpus/skills/csharp-refactoring/NOTICE +22 -0
- package/package/corpus/skills/csharp-refactoring/SKILL.md +175 -0
- package/package/corpus/skills/csharp-refactoring/references/operation-catalog.md +37 -0
- package/package/corpus/skills/directory-build-organization/LICENSE +23 -0
- package/package/corpus/skills/directory-build-organization/NOTICE +21 -0
- package/package/corpus/skills/directory-build-organization/SKILL.md +205 -0
- package/package/corpus/skills/directory-build-organization/references/common-patterns.md +56 -0
- package/package/corpus/skills/directory-build-organization/references/multi-level-examples.md +167 -0
- package/package/corpus/skills/directory-build-organization/references/targetframework-props-pitfall.md +54 -0
- package/package/corpus/skills/dotnet-aot-compat/LICENSE +23 -0
- package/package/corpus/skills/dotnet-aot-compat/NOTICE +18 -0
- package/package/corpus/skills/dotnet-aot-compat/SKILL.md +265 -0
- package/package/corpus/skills/dotnet-aot-compat/references/polyfills.md +43 -0
- package/package/corpus/skills/dotnet-trace-collect/LICENSE +23 -0
- package/package/corpus/skills/dotnet-trace-collect/NOTICE +65 -0
- package/package/corpus/skills/dotnet-trace-collect/SKILL.md +116 -0
- package/package/corpus/skills/dotnet-trace-collect/references/dotnet-monitor.md +97 -0
- package/package/corpus/skills/dotnet-trace-collect/references/dotnet-trace-collect-linux.md +84 -0
- package/package/corpus/skills/dotnet-trace-collect/references/dotnet-trace-collect.md +91 -0
- package/package/corpus/skills/dotnet-trace-collect/references/perfcollect.md +200 -0
- package/package/corpus/skills/dotnet-trace-collect/references/perfview.md +141 -0
- package/package/corpus/skills/dotnet-trace-collect/references/tool-selection.md +160 -0
- package/package/corpus/skills/dump-collect/LICENSE +23 -0
- package/package/corpus/skills/dump-collect/NOTICE +40 -0
- package/package/corpus/skills/dump-collect/SKILL.md +97 -0
- package/package/corpus/skills/dump-collect/references/container-dumps.md +412 -0
- package/package/corpus/skills/dump-collect/references/coreclr-dumps.md +108 -0
- package/package/corpus/skills/dump-collect/references/nativeaot-dumps.md +280 -0
- package/package/corpus/skills/eliminate-redundant-restores/LICENSE +23 -0
- package/package/corpus/skills/eliminate-redundant-restores/NOTICE +19 -0
- package/package/corpus/skills/eliminate-redundant-restores/SKILL.md +112 -0
- package/package/corpus/skills/extension-points/LICENSE +23 -0
- package/package/corpus/skills/extension-points/NOTICE +18 -0
- package/package/corpus/skills/extension-points/SKILL.md +248 -0
- package/package/corpus/skills/filter-syntax/LICENSE +23 -0
- package/package/corpus/skills/filter-syntax/NOTICE +17 -0
- package/package/corpus/skills/filter-syntax/SKILL.md +179 -0
- package/package/corpus/skills/including-generated-files/LICENSE +23 -0
- package/package/corpus/skills/including-generated-files/NOTICE +17 -0
- package/package/corpus/skills/including-generated-files/SKILL.md +139 -0
- package/package/corpus/skills/microbenchmarking/LICENSE +23 -0
- package/package/corpus/skills/microbenchmarking/NOTICE +36 -0
- package/package/corpus/skills/microbenchmarking/SKILL.md +131 -0
- package/package/corpus/skills/microbenchmarking/references/bdn-internals-and-tuning.md +143 -0
- package/package/corpus/skills/microbenchmarking/references/comparison-strategies.md +200 -0
- package/package/corpus/skills/microbenchmarking/references/diagnosers-and-exporters.md +150 -0
- package/package/corpus/skills/microbenchmarking/references/project-setup-and-running.md +189 -0
- package/package/corpus/skills/microbenchmarking/references/writing-benchmarks.md +310 -0
- package/package/corpus/skills/msbuild/LICENSE +23 -0
- package/package/corpus/skills/msbuild/NOTICE +55 -0
- package/package/corpus/skills/msbuild/SKILL.md +117 -0
- package/package/corpus/skills/msbuild/references/antipatterns/additional-antipatterns.md +291 -0
- package/package/corpus/skills/msbuild/references/antipatterns/incremental-build-inputs-outputs.md +57 -0
- package/package/corpus/skills/msbuild/references/antipatterns/private-assets.md +29 -0
- package/package/corpus/skills/msbuild/references/binlog-failure-analysis.md +85 -0
- package/package/corpus/skills/msbuild/references/binlog-generation.md +69 -0
- package/package/corpus/skills/msbuild/references/build-perf-baseline.md +493 -0
- package/package/corpus/skills/msbuild/references/build-perf-diagnostics.md +268 -0
- package/package/corpus/skills/msbuild/references/copy-to-output-directory.md +183 -0
- package/package/corpus/skills/msbuild/references/eval-performance.md +190 -0
- package/package/corpus/skills/msbuild/references/extension-points.md +281 -0
- package/package/corpus/skills/msbuild/references/incremental-build.md +365 -0
- package/package/corpus/skills/msbuild/references/msbuild-antipatterns.md +401 -0
- package/package/corpus/skills/orleans/LICENSE +21 -0
- package/package/corpus/skills/orleans/NOTICE +64 -0
- package/package/corpus/skills/orleans/SKILL.md +140 -0
- package/package/corpus/skills/orleans/references/anti-patterns.md +660 -0
- package/package/corpus/skills/orleans/references/configuration-api.md +933 -0
- package/package/corpus/skills/orleans/references/examples.md +66 -0
- package/package/corpus/skills/orleans/references/grain-api.md +784 -0
- package/package/corpus/skills/orleans/references/grains.md +75 -0
- package/package/corpus/skills/orleans/references/hosting.md +71 -0
- package/package/corpus/skills/orleans/references/implementation.md +591 -0
- package/package/corpus/skills/orleans/references/official-docs-index.md +250 -0
- package/package/corpus/skills/orleans/references/patterns.md +811 -0
- package/package/corpus/skills/orleans/references/persistence-api.md +495 -0
- package/package/corpus/skills/orleans/references/primitive-selection.md +35 -0
- package/package/corpus/skills/orleans/references/scheduling-and-services.md +295 -0
- package/package/corpus/skills/orleans/references/serialization-api.md +299 -0
- package/package/corpus/skills/orleans/references/streaming-api.md +396 -0
- package/package/corpus/skills/orleans/references/testing-patterns.md +210 -0
- package/package/corpus/skills/platform-detection/LICENSE +23 -0
- package/package/corpus/skills/platform-detection/NOTICE +17 -0
- package/package/corpus/skills/platform-detection/SKILL.md +230 -0
- package/package/corpus/skills/platform-detection/references/command-mode.md +27 -0
- package/package/corpus/skills/profiling/LICENSE +21 -0
- package/package/corpus/skills/profiling/NOTICE +50 -0
- package/package/corpus/skills/profiling/SKILL.md +144 -0
- package/package/corpus/skills/profiling/references/commands.md +439 -0
- package/package/corpus/skills/profiling/references/patterns.md +586 -0
- package/package/corpus/skills/profiling/references/profiling.md +190 -0
- package/package/corpus/skills/run-tests/LICENSE +23 -0
- package/package/corpus/skills/run-tests/NOTICE +19 -0
- package/package/corpus/skills/run-tests/SKILL.md +305 -0
- package/package/corpus/skills/target-authoring/LICENSE +23 -0
- package/package/corpus/skills/target-authoring/NOTICE +17 -0
- package/package/corpus/skills/target-authoring/SKILL.md +165 -0
- package/package/corpus/skills/vectorization/LICENSE +23 -0
- package/package/corpus/skills/vectorization/NOTICE +17 -0
- package/package/corpus/skills/vectorization/SKILL.md +189 -0
- package/package/profile-catalog.json +87 -1
- package/package.json +3 -2
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Async & Concurrency Patterns
|
|
2
|
+
|
|
3
|
+
## Contents
|
|
4
|
+
|
|
5
|
+
- Don't Expose Async Wrappers for Sync Methods
|
|
6
|
+
- Don't Expose Sync Wrappers for Async Methods
|
|
7
|
+
- Use ValueTask for Hot Paths with Frequent Sync Completion
|
|
8
|
+
- Use Channels for Producer/Consumer
|
|
9
|
+
- Avoid False Sharing with Thread-Local State
|
|
10
|
+
- Detection
|
|
11
|
+
- Patterns Requiring Manual Review
|
|
12
|
+
|
|
13
|
+
### Don't Expose Async Wrappers for Sync Methods
|
|
14
|
+
🟡 **AVOID** wrapping sync methods with `Task.Run` in libraries | .NET Core+
|
|
15
|
+
|
|
16
|
+
❌
|
|
17
|
+
```csharp
|
|
18
|
+
public Task<int> ComputeHashAsync(byte[] data) =>
|
|
19
|
+
Task.Run(() => ComputeHash(data));
|
|
20
|
+
```
|
|
21
|
+
✅
|
|
22
|
+
```csharp
|
|
23
|
+
public int ComputeHash(byte[] data) { /* CPU-bound work */ }
|
|
24
|
+
// Consumer decides: var hash = await Task.Run(() => lib.ComputeHash(data));
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**Impact: Eliminates unnecessary thread pool queue/dequeue overhead per call.**
|
|
28
|
+
|
|
29
|
+
### Don't Expose Sync Wrappers for Async Methods
|
|
30
|
+
🟡 **AVOID** creating sync wrappers that block on async implementations | .NET Core+
|
|
31
|
+
|
|
32
|
+
❌
|
|
33
|
+
```csharp
|
|
34
|
+
public string GetData() => GetDataAsync().Result;
|
|
35
|
+
```
|
|
36
|
+
✅
|
|
37
|
+
```csharp
|
|
38
|
+
public async Task<string> GetDataAsync() { /* ... */ }
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Impact: Prevents deadlocks and thread pool starvation from hidden sync-over-async blocking.**
|
|
42
|
+
|
|
43
|
+
### Use ValueTask for Hot Paths with Frequent Sync Completion
|
|
44
|
+
🟡 **DO** use `ValueTask<T>` on hot paths where sync completion is common | .NET Core 2.1+
|
|
45
|
+
|
|
46
|
+
❌
|
|
47
|
+
```csharp
|
|
48
|
+
public async Task<int> ReadAsync(Memory<byte> buffer)
|
|
49
|
+
{
|
|
50
|
+
if (_bufferedCount > 0)
|
|
51
|
+
return ReadFromBuffer(buffer.Span);
|
|
52
|
+
return await ReadAsyncCore(buffer);
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
✅
|
|
56
|
+
```csharp
|
|
57
|
+
public ValueTask<int> ReadAsync(Memory<byte> buffer)
|
|
58
|
+
{
|
|
59
|
+
if (_bufferedCount > 0)
|
|
60
|
+
return new ValueTask<int>(ReadFromBuffer(buffer.Span));
|
|
61
|
+
return new ValueTask<int>(ReadAsyncCore(buffer));
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Impact: Eliminates Task\<T\> allocation on synchronous completion — the struct stores results inline.**
|
|
66
|
+
|
|
67
|
+
### Use Channels for Producer/Consumer
|
|
68
|
+
🟡 **DO** use `System.Threading.Channels` for producer-consumer patterns | .NET Core 3.0+
|
|
69
|
+
|
|
70
|
+
❌
|
|
71
|
+
```csharp
|
|
72
|
+
var queue = new BlockingCollection<WorkItem>();
|
|
73
|
+
var item = queue.Take();
|
|
74
|
+
```
|
|
75
|
+
✅
|
|
76
|
+
```csharp
|
|
77
|
+
var channel = Channel.CreateUnbounded<WorkItem>();
|
|
78
|
+
|
|
79
|
+
// Producer
|
|
80
|
+
await channel.Writer.WriteAsync(item);
|
|
81
|
+
|
|
82
|
+
// Consumer
|
|
83
|
+
await foreach (var item in channel.Reader.ReadAllAsync())
|
|
84
|
+
Process(item);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**Impact: ~25% faster, ~95% fewer GC collections vs manual approaches.**
|
|
88
|
+
|
|
89
|
+
### Avoid False Sharing with Thread-Local State
|
|
90
|
+
🟡 **AVOID** adjacent mutable fields written by different threads | .NET 7+
|
|
91
|
+
|
|
92
|
+
❌
|
|
93
|
+
```csharp
|
|
94
|
+
class SharedCounters
|
|
95
|
+
{
|
|
96
|
+
public long Counter1;
|
|
97
|
+
public long Counter2;
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
✅
|
|
101
|
+
```csharp
|
|
102
|
+
[StructLayout(LayoutKind.Explicit, Size = 128)]
|
|
103
|
+
struct PaddedCounter
|
|
104
|
+
{
|
|
105
|
+
[FieldOffset(0)] public long Value;
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**Impact: Eliminates cross-core cache invalidation — can improve multi-threaded throughput by 10x+.**
|
|
110
|
+
|
|
111
|
+
## Detection
|
|
112
|
+
|
|
113
|
+
Scan recipes for async anti-patterns. Run these and report exact counts.
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
# async void methods (correctness issue — crashes on exception)
|
|
117
|
+
grep -rn --include='*.cs' 'async void' --exclude-dir=bin --exclude-dir=obj . | grep -v 'event' | wc -l
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Patterns Requiring Manual Review
|
|
121
|
+
|
|
122
|
+
- **Sync-over-async** (`.Result`, `.Wait()`): `.Result` matches any property named Result — needs type context to confirm it's `Task.Result`
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# Collections & LINQ Patterns
|
|
2
|
+
|
|
3
|
+
## Contents
|
|
4
|
+
|
|
5
|
+
- Use FrozenDictionary/FrozenSet for Read-Heavy Lookup Tables
|
|
6
|
+
- Use Dictionary Alternate Lookup for Span-Based Keys
|
|
7
|
+
- Use CollectionsMarshal.GetValueRefOrNullRef for Lookup-and-Update
|
|
8
|
+
- Use Collection Expressions [] for Zero-Allocation Span Creation
|
|
9
|
+
- Use EnsureCapacity on List/Stack/Queue Before Bulk Adds
|
|
10
|
+
- Use TryGetNonEnumeratedCount for Pre-Sizing
|
|
11
|
+
- Hoist Static Data Out of Method Bodies
|
|
12
|
+
- Add Overloads to Avoid params Array Allocation
|
|
13
|
+
- Detection
|
|
14
|
+
- Patterns Requiring Manual Review
|
|
15
|
+
|
|
16
|
+
### Use FrozenDictionary/FrozenSet for Read-Heavy Lookup Tables
|
|
17
|
+
🟡 **DO** use `FrozenDictionary`/`FrozenSet` for collections created once and read many times | .NET 8+
|
|
18
|
+
|
|
19
|
+
❌
|
|
20
|
+
```csharp
|
|
21
|
+
private static readonly Dictionary<string, int> s_statusCodes = new()
|
|
22
|
+
{
|
|
23
|
+
["OK"] = 200, ["NotFound"] = 404, ["InternalServerError"] = 500
|
|
24
|
+
};
|
|
25
|
+
```
|
|
26
|
+
✅
|
|
27
|
+
```csharp
|
|
28
|
+
private static readonly FrozenDictionary<string, int> s_statusCodes =
|
|
29
|
+
new Dictionary<string, int>
|
|
30
|
+
{
|
|
31
|
+
["OK"] = 200, ["NotFound"] = 404, ["InternalServerError"] = 500
|
|
32
|
+
}.ToFrozenDictionary();
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Impact: ~50% faster lookups than Dictionary, ~14x faster than ImmutableDictionary.**
|
|
36
|
+
|
|
37
|
+
### Use Dictionary Alternate Lookup for Span-Based Keys
|
|
38
|
+
🟡 **DO** use `GetAlternateLookup<ReadOnlySpan<char>>()` to avoid string allocation on lookups | .NET 9+
|
|
39
|
+
|
|
40
|
+
❌
|
|
41
|
+
```csharp
|
|
42
|
+
string key = headerLine.Substring(0, colonIndex);
|
|
43
|
+
if (s_dict.TryGetValue(key, out int value)) { /* ... */ }
|
|
44
|
+
```
|
|
45
|
+
✅
|
|
46
|
+
```csharp
|
|
47
|
+
var lookup = s_dict.GetAlternateLookup<ReadOnlySpan<char>>();
|
|
48
|
+
ReadOnlySpan<char> key = headerLine.AsSpan(0, colonIndex);
|
|
49
|
+
if (lookup.TryGetValue(key, out int value)) { }
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**Impact: Avoids string allocation per lookup — especially valuable in parser/protocol hot paths.**
|
|
53
|
+
|
|
54
|
+
### Use CollectionsMarshal.GetValueRefOrNullRef for Lookup-and-Update
|
|
55
|
+
🟡 **DO** use `CollectionsMarshal.GetValueRefOrAddDefault` for dictionary update patterns | .NET 6+
|
|
56
|
+
|
|
57
|
+
❌
|
|
58
|
+
```csharp
|
|
59
|
+
_counts.TryGetValue(key, out int count);
|
|
60
|
+
_counts[key] = count + 1;
|
|
61
|
+
```
|
|
62
|
+
✅
|
|
63
|
+
```csharp
|
|
64
|
+
ref int count = ref CollectionsMarshal.GetValueRefOrAddDefault(_counts, key, out _);
|
|
65
|
+
count++;
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Impact: ~48% faster for lookup-and-update patterns (95µs → 49µs).**
|
|
69
|
+
|
|
70
|
+
### Use Collection Expressions [] for Zero-Allocation Span Creation
|
|
71
|
+
🟡 **DO** use collection expressions for `Span<T>` targets | C# 12 / .NET 8+
|
|
72
|
+
|
|
73
|
+
❌
|
|
74
|
+
```csharp
|
|
75
|
+
int[] values = new int[] { a, b, c, d };
|
|
76
|
+
```
|
|
77
|
+
✅
|
|
78
|
+
```csharp
|
|
79
|
+
Span<int> values = [a, b, c, d];
|
|
80
|
+
ReadOnlySpan<int> daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Impact: Zero heap allocation for span-targeted collection expressions.**
|
|
84
|
+
|
|
85
|
+
### Use EnsureCapacity on List/Stack/Queue Before Bulk Adds
|
|
86
|
+
🟡 **DO** call `EnsureCapacity` before bulk insertions | .NET 6+
|
|
87
|
+
|
|
88
|
+
❌
|
|
89
|
+
```csharp
|
|
90
|
+
var list = new List<int>();
|
|
91
|
+
for (int i = 0; i < 10000; i++)
|
|
92
|
+
list.Add(i);
|
|
93
|
+
```
|
|
94
|
+
✅
|
|
95
|
+
```csharp
|
|
96
|
+
var list = new List<int>();
|
|
97
|
+
list.EnsureCapacity(10000);
|
|
98
|
+
for (int i = 0; i < 10000; i++)
|
|
99
|
+
list.Add(i);
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Impact: Reduces reallocations and array copies during bulk operations.**
|
|
103
|
+
|
|
104
|
+
### Use TryGetNonEnumeratedCount for Pre-Sizing
|
|
105
|
+
🟡 **DO** use `TryGetNonEnumeratedCount` to pre-size destination collections | .NET 6+
|
|
106
|
+
|
|
107
|
+
❌
|
|
108
|
+
```csharp
|
|
109
|
+
var results = new List<int>();
|
|
110
|
+
foreach (var item in source)
|
|
111
|
+
results.Add(Transform(item));
|
|
112
|
+
```
|
|
113
|
+
✅
|
|
114
|
+
```csharp
|
|
115
|
+
var results = source.TryGetNonEnumeratedCount(out int count)
|
|
116
|
+
? new List<int>(count)
|
|
117
|
+
: new List<int>();
|
|
118
|
+
foreach (var item in source)
|
|
119
|
+
results.Add(Transform(item));
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**Impact: Avoids O(n) enumeration for counting; eliminates resizing allocations.**
|
|
123
|
+
|
|
124
|
+
### Hoist Static Data Out of Method Bodies
|
|
125
|
+
🟡 **AVOID** creating collections with static/deterministic data inside method bodies | .NET Core+
|
|
126
|
+
|
|
127
|
+
❌
|
|
128
|
+
```csharp
|
|
129
|
+
public string Convert(long number)
|
|
130
|
+
{
|
|
131
|
+
var groupsMap = new Dictionary<long, Func<long, string>>
|
|
132
|
+
{
|
|
133
|
+
{ 1_000_000_000, n => $"{Convert(n)} billion" },
|
|
134
|
+
{ 1_000_000, n => $"{Convert(n)} million" },
|
|
135
|
+
{ 1_000, n => $"{Convert(n)} thousand" },
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
✅
|
|
140
|
+
```csharp
|
|
141
|
+
private static readonly FrozenDictionary<long, Func<long, string>> s_groupsMap =
|
|
142
|
+
new Dictionary<long, Func<long, string>>
|
|
143
|
+
{
|
|
144
|
+
{ 1_000_000_000, n => $"{Convert(n)} billion" },
|
|
145
|
+
{ 1_000_000, n => $"{Convert(n)} million" },
|
|
146
|
+
{ 1_000, n => $"{Convert(n)} thousand" },
|
|
147
|
+
}.ToFrozenDictionary();
|
|
148
|
+
|
|
149
|
+
public string Convert(long number)
|
|
150
|
+
{
|
|
151
|
+
// ... use s_groupsMap
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**Impact: Eliminates collection + internal storage + closure allocations per call. For a Dictionary with N entries, saves ~N+3 allocations per invocation.**
|
|
156
|
+
|
|
157
|
+
### Add Overloads to Avoid params Array Allocation
|
|
158
|
+
🟡 **DO** add 1- and 2-argument overloads for methods that accept `params T[]`, or use `params ReadOnlySpan<T>` on .NET 9+ | .NET Core+
|
|
159
|
+
|
|
160
|
+
❌
|
|
161
|
+
```csharp
|
|
162
|
+
public static string Transform(this string input, params IStringTransformer[] transformers) =>
|
|
163
|
+
transformers.Aggregate(input, (current, t) => t.Transform(current));
|
|
164
|
+
|
|
165
|
+
"hello".Transform(To.TitleCase);
|
|
166
|
+
```
|
|
167
|
+
✅
|
|
168
|
+
```csharp
|
|
169
|
+
// Option A: Explicit overloads for common arities
|
|
170
|
+
public static string Transform(this string input, IStringTransformer transformer) =>
|
|
171
|
+
transformer.Transform(input);
|
|
172
|
+
|
|
173
|
+
public static string Transform(this string input, IStringTransformer t1, IStringTransformer t2) =>
|
|
174
|
+
t2.Transform(t1.Transform(input));
|
|
175
|
+
|
|
176
|
+
public static string Transform(this string input, params IStringTransformer[] transformers) =>
|
|
177
|
+
transformers.Aggregate(input, (current, t) => t.Transform(current));
|
|
178
|
+
|
|
179
|
+
// Option B (.NET 9+ / C# 13): params ReadOnlySpan<T> — zero heap allocation
|
|
180
|
+
public static string Transform(this string input, params ReadOnlySpan<IStringTransformer> transformers)
|
|
181
|
+
{
|
|
182
|
+
foreach (var t in transformers)
|
|
183
|
+
input = t.Transform(input);
|
|
184
|
+
return input;
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**Impact: Eliminates one array allocation per call for the common 1- and 2-argument cases. `params ReadOnlySpan<T>` eliminates it for all arities.**
|
|
189
|
+
|
|
190
|
+
## Detection
|
|
191
|
+
|
|
192
|
+
Scan recipes for collection and LINQ anti-patterns. Run these and report exact counts.
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
# Static Dictionary not using FrozenDictionary (read-only after init)
|
|
196
|
+
grep -rn --include='*.cs' 'static readonly Dictionary<' --exclude-dir=bin --exclude-dir=obj . | wc -l
|
|
197
|
+
|
|
198
|
+
# Static FrozenDictionary (already optimized — verify the inverse)
|
|
199
|
+
grep -rn --include='*.cs' 'static readonly FrozenDictionary<' --exclude-dir=bin --exclude-dir=obj . | wc -l
|
|
200
|
+
|
|
201
|
+
# Per-call List allocation (inside method bodies, not static/readonly fields)
|
|
202
|
+
grep -rn --include='*.cs' 'new List<' --exclude-dir=bin --exclude-dir=obj . | grep -v 'static\|readonly' | wc -l
|
|
203
|
+
|
|
204
|
+
# Per-call Dictionary allocation (inside method bodies, not static/readonly fields)
|
|
205
|
+
grep -rn --include='*.cs' 'new Dictionary<' --exclude-dir=bin --exclude-dir=obj . | grep -v 'static\|readonly' | wc -l
|
|
206
|
+
|
|
207
|
+
# StringComparer.CurrentCulture usage (almost always wrong in library code — use Ordinal)
|
|
208
|
+
grep -rn --include='*.cs' 'StringComparer.CurrentCulture' --exclude-dir=bin --exclude-dir=obj . | wc -l
|
|
209
|
+
|
|
210
|
+
# LINQ chains in extension/hot-path files (.Select, .Where, .Cast, .Take, .Aggregate)
|
|
211
|
+
grep -rn --include='*.cs' -E '\.(Select|Where|Cast|Take|Aggregate)\(' --exclude-dir=bin --exclude-dir=obj . | wc -l
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
For the LINQ chain recipe: any hit in a file whose name ends in `Extensions.cs`, `Formatter.cs`, or implements a method called from a public extension method is a hot-path candidate. Inspect each hit in these files and flag LINQ chains that allocate delegates, enumerators, or intermediate collections on every call. Hits in localization converters or one-time initialization are lower priority.
|
|
215
|
+
|
|
216
|
+
### Patterns Requiring Manual Review
|
|
217
|
+
|
|
218
|
+
- **ContainsKey + indexer double-lookup**: Requires verifying the same key is used in a subsequent indexer access — multi-line/multi-statement context
|
|
219
|
+
- **LINQ on hot paths**: The LINQ chain recipe above catches call sites, but distinguishing hot-path from cold-path requires context. Prioritize hits in `*Extensions.cs` and `*Formatter.cs` files, which are typically called on every user invocation
|
|
220
|
+
- **`new Dictionary/List<` in method bodies vs fields**: The grep heuristic (`grep -v 'static\|readonly'`) catches most cases but may include false positives from field initializers without `static`/`readonly` — spot-check flagged lines
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
# Critical .NET Performance Anti-Patterns
|
|
2
|
+
|
|
3
|
+
## Contents
|
|
4
|
+
|
|
5
|
+
- Async / Tasks
|
|
6
|
+
- Memory / Allocation
|
|
7
|
+
- Strings
|
|
8
|
+
- Regular Expressions
|
|
9
|
+
- JSON Serialization
|
|
10
|
+
- Networking
|
|
11
|
+
- General
|
|
12
|
+
- Detection
|
|
13
|
+
|
|
14
|
+
17 patterns that cause deadlocks, order-of-magnitude regressions, or excessive allocations.
|
|
15
|
+
|
|
16
|
+
## Async / Tasks
|
|
17
|
+
|
|
18
|
+
### Never Block on Async (Sync-over-Async)
|
|
19
|
+
🔴 **AVOID** | .NET Core+
|
|
20
|
+
|
|
21
|
+
❌
|
|
22
|
+
```csharp
|
|
23
|
+
public string GetData()
|
|
24
|
+
=> GetDataAsync().Result;
|
|
25
|
+
```
|
|
26
|
+
✅
|
|
27
|
+
```csharp
|
|
28
|
+
public async Task<string> GetDataAsync()
|
|
29
|
+
=> await GetDataInternalAsync();
|
|
30
|
+
```
|
|
31
|
+
**Impact: Deadlocks or thread pool starvation; wastes threads, destroys scalability.**
|
|
32
|
+
|
|
33
|
+
### Never Await a ValueTask Multiple Times
|
|
34
|
+
🔴 **AVOID** | .NET Core 2.1+
|
|
35
|
+
|
|
36
|
+
❌
|
|
37
|
+
```csharp
|
|
38
|
+
ValueTask<int> vt = SomeMethodAsync();
|
|
39
|
+
int a = await vt;
|
|
40
|
+
int b = await vt;
|
|
41
|
+
```
|
|
42
|
+
✅
|
|
43
|
+
```csharp
|
|
44
|
+
int result = await SomeMethodAsync();
|
|
45
|
+
```
|
|
46
|
+
**Impact: Undefined behavior — silent data corruption or exceptions.**
|
|
47
|
+
|
|
48
|
+
## Memory / Allocation
|
|
49
|
+
|
|
50
|
+
### Use Span\<T\> / AsSpan Instead of Substring for Slicing
|
|
51
|
+
🔴 **DO** | .NET Core 2.1+
|
|
52
|
+
|
|
53
|
+
❌
|
|
54
|
+
```csharp
|
|
55
|
+
string sub = input.Substring(5, 10);
|
|
56
|
+
```
|
|
57
|
+
✅
|
|
58
|
+
```csharp
|
|
59
|
+
ReadOnlySpan<char> sub = input.AsSpan(5, 10);
|
|
60
|
+
```
|
|
61
|
+
**Impact: Eliminates per-slice allocations; 2-4x faster via vectorization.**
|
|
62
|
+
|
|
63
|
+
### Use ArrayPool\<T\> for Temporary Buffers
|
|
64
|
+
🔴 **DO** | .NET Core+
|
|
65
|
+
|
|
66
|
+
❌
|
|
67
|
+
```csharp
|
|
68
|
+
byte[] buf = new byte[4096];
|
|
69
|
+
```
|
|
70
|
+
✅
|
|
71
|
+
```csharp
|
|
72
|
+
byte[] buf = ArrayPool<byte>.Shared.Rent(4096);
|
|
73
|
+
Process(buf);
|
|
74
|
+
ArrayPool<byte>.Shared.Return(buf);
|
|
75
|
+
```
|
|
76
|
+
**Impact: Dramatically reduces GC pressure for buffer-heavy workloads.**
|
|
77
|
+
|
|
78
|
+
### Avoid stackalloc in Loops
|
|
79
|
+
🔴 **AVOID** | .NET 5+
|
|
80
|
+
|
|
81
|
+
❌
|
|
82
|
+
```csharp
|
|
83
|
+
for (int i = 0; i < 10_000; i++)
|
|
84
|
+
Span<byte> buf = stackalloc byte[1024];
|
|
85
|
+
```
|
|
86
|
+
✅
|
|
87
|
+
```csharp
|
|
88
|
+
Span<byte> buf = stackalloc byte[1024];
|
|
89
|
+
for (int i = 0; i < 10_000; i++) { Process(buf); }
|
|
90
|
+
```
|
|
91
|
+
**Impact: StackOverflowException — unrecoverable, no catch possible.**
|
|
92
|
+
|
|
93
|
+
### Avoid Boxing Value Types
|
|
94
|
+
🔴 **AVOID** | .NET 6+
|
|
95
|
+
|
|
96
|
+
❌
|
|
97
|
+
```csharp
|
|
98
|
+
string s = string.Format("{0}.{1}", major, minor);
|
|
99
|
+
```
|
|
100
|
+
✅
|
|
101
|
+
```csharp
|
|
102
|
+
string s = $"{major}.{minor}";
|
|
103
|
+
```
|
|
104
|
+
**Impact: When replacing `string.Format` with C# 10+ interpolation, typical improvements are ~40% faster with significantly less allocation. Actual gains vary by call site.**
|
|
105
|
+
|
|
106
|
+
## Strings
|
|
107
|
+
|
|
108
|
+
### Use StringComparison.Ordinal for Non-Linguistic Comparisons
|
|
109
|
+
🔴 **DO** | .NET Core+
|
|
110
|
+
|
|
111
|
+
❌
|
|
112
|
+
```csharp
|
|
113
|
+
bool found = text.IndexOf("Content-Type") >= 0;
|
|
114
|
+
```
|
|
115
|
+
✅
|
|
116
|
+
```csharp
|
|
117
|
+
bool found = text.Contains("Content-Type", StringComparison.Ordinal);
|
|
118
|
+
```
|
|
119
|
+
**Impact: 2-3x faster; OrdinalIgnoreCase hash codes ~3.3x faster.**
|
|
120
|
+
|
|
121
|
+
### Use AsSpan Instead of Substring
|
|
122
|
+
🔴 **DO** | .NET Core 2.1+
|
|
123
|
+
|
|
124
|
+
❌
|
|
125
|
+
```csharp
|
|
126
|
+
int val = int.Parse(str.Substring(5, 3));
|
|
127
|
+
```
|
|
128
|
+
✅
|
|
129
|
+
```csharp
|
|
130
|
+
int val = int.Parse(str.AsSpan(5, 3));
|
|
131
|
+
```
|
|
132
|
+
**Impact: Eliminates one string allocation per parse operation.**
|
|
133
|
+
|
|
134
|
+
## Regular Expressions
|
|
135
|
+
|
|
136
|
+
### Use Source-Generated Regex [GeneratedRegex]
|
|
137
|
+
🔴 **ALWAYS** use `[GeneratedRegex]` for all static regex patterns | .NET 7+
|
|
138
|
+
|
|
139
|
+
❌
|
|
140
|
+
```csharp
|
|
141
|
+
private static readonly Regex s_re =
|
|
142
|
+
new(@"\w+@\w+\.\w+", RegexOptions.Compiled);
|
|
143
|
+
```
|
|
144
|
+
✅
|
|
145
|
+
```csharp
|
|
146
|
+
[GeneratedRegex(@"\w+@\w+\.\w+")]
|
|
147
|
+
private static partial Regex EmailRegex();
|
|
148
|
+
```
|
|
149
|
+
**Impact: Always beneficial or neutral for static patterns — near-zero startup, better throughput, and required for AOT/trimming scenarios.**
|
|
150
|
+
|
|
151
|
+
### Avoid Nested Quantifiers (Catastrophic Backtracking)
|
|
152
|
+
🔴 **AVOID** | .NET Core+
|
|
153
|
+
|
|
154
|
+
❌
|
|
155
|
+
```csharp
|
|
156
|
+
var r = new Regex(@"^(\w+)+$");
|
|
157
|
+
```
|
|
158
|
+
✅
|
|
159
|
+
```csharp
|
|
160
|
+
var r = new Regex(@"^\w+$", RegexOptions.NonBacktracking);
|
|
161
|
+
```
|
|
162
|
+
**Impact: Can hang process indefinitely on crafted input.**
|
|
163
|
+
|
|
164
|
+
### Use TryGetValue Instead of ContainsKey + Indexer
|
|
165
|
+
🔴 **DO** | .NET Core+
|
|
166
|
+
|
|
167
|
+
❌
|
|
168
|
+
```csharp
|
|
169
|
+
if (dict.ContainsKey(key))
|
|
170
|
+
Use(dict[key]);
|
|
171
|
+
```
|
|
172
|
+
✅
|
|
173
|
+
```csharp
|
|
174
|
+
if (dict.TryGetValue(key, out var value))
|
|
175
|
+
Use(value);
|
|
176
|
+
```
|
|
177
|
+
**Impact: ~2x faster (50% reduction in lookup time).**
|
|
178
|
+
|
|
179
|
+
### Avoid LINQ in Hot Paths
|
|
180
|
+
🔴 **AVOID** | .NET Core+
|
|
181
|
+
|
|
182
|
+
❌
|
|
183
|
+
```csharp
|
|
184
|
+
bool found = items.Any(x => x.Name == target);
|
|
185
|
+
```
|
|
186
|
+
✅
|
|
187
|
+
```csharp
|
|
188
|
+
bool found = false;
|
|
189
|
+
foreach (var item in items)
|
|
190
|
+
if (item.Name == target) { found = true; break; }
|
|
191
|
+
```
|
|
192
|
+
**Impact: Eliminates 1-3 allocations per call; measurable in tight loops.**
|
|
193
|
+
|
|
194
|
+
### Don't Iterate IEnumerable Multiple Times
|
|
195
|
+
🔴 **AVOID** | .NET Core+
|
|
196
|
+
|
|
197
|
+
❌
|
|
198
|
+
```csharp
|
|
199
|
+
foreach (Type t in types) { Validate(t); }
|
|
200
|
+
_types = types.ToArray();
|
|
201
|
+
```
|
|
202
|
+
✅
|
|
203
|
+
```csharp
|
|
204
|
+
Type[] arr = types.ToArray();
|
|
205
|
+
foreach (Type t in arr) { Validate(t); }
|
|
206
|
+
_types = arr;
|
|
207
|
+
```
|
|
208
|
+
**Impact: Halves enumeration cost; prevents bugs from re-executing deferred queries.**
|
|
209
|
+
|
|
210
|
+
## JSON Serialization
|
|
211
|
+
|
|
212
|
+
### Use System.Text.Json Source Generator
|
|
213
|
+
🔴 **DO** | .NET 6+
|
|
214
|
+
|
|
215
|
+
❌
|
|
216
|
+
```csharp
|
|
217
|
+
string json = JsonSerializer.Serialize(post);
|
|
218
|
+
```
|
|
219
|
+
✅
|
|
220
|
+
```csharp
|
|
221
|
+
[JsonSerializable(typeof(BlogPost))]
|
|
222
|
+
internal partial class AppJsonCtx : JsonSerializerContext { }
|
|
223
|
+
string json = JsonSerializer.Serialize(post, AppJsonCtx.Default.BlogPost);
|
|
224
|
+
```
|
|
225
|
+
**Impact: 37-44% faster; enables trimming and Native AOT.**
|
|
226
|
+
|
|
227
|
+
### Cache JsonSerializerOptions
|
|
228
|
+
🔴 **DO** | .NET 5+
|
|
229
|
+
|
|
230
|
+
❌
|
|
231
|
+
```csharp
|
|
232
|
+
JsonSerializer.Serialize(obj, new JsonSerializerOptions());
|
|
233
|
+
```
|
|
234
|
+
✅
|
|
235
|
+
```csharp
|
|
236
|
+
private static readonly JsonSerializerOptions s_opts = new();
|
|
237
|
+
JsonSerializer.Serialize(obj, s_opts);
|
|
238
|
+
```
|
|
239
|
+
**Impact: Up to 592x slower without caching (.NET 6); always cache or use defaults.**
|
|
240
|
+
|
|
241
|
+
## Networking
|
|
242
|
+
|
|
243
|
+
### Reuse HttpClient Instances
|
|
244
|
+
🔴 **DO** | .NET Core 2.1+
|
|
245
|
+
|
|
246
|
+
❌
|
|
247
|
+
```csharp
|
|
248
|
+
using var client = new HttpClient();
|
|
249
|
+
await client.GetStringAsync(url);
|
|
250
|
+
```
|
|
251
|
+
✅
|
|
252
|
+
```csharp
|
|
253
|
+
private static readonly HttpClient s_http = new(new SocketsHttpHandler
|
|
254
|
+
{ PooledConnectionLifetime = TimeSpan.FromMinutes(5) });
|
|
255
|
+
await s_http.GetStringAsync(url);
|
|
256
|
+
```
|
|
257
|
+
**Impact: Prevents socket exhaustion; 6-12x faster concurrent HTTPS.**
|
|
258
|
+
|
|
259
|
+
## General
|
|
260
|
+
|
|
261
|
+
### Use SearchValues\<T\> for Repeated Set Searches
|
|
262
|
+
🔴 **DO** | .NET 8+
|
|
263
|
+
|
|
264
|
+
❌
|
|
265
|
+
```csharp
|
|
266
|
+
int pos = text.IndexOfAny("ABCDEF".ToCharArray());
|
|
267
|
+
```
|
|
268
|
+
✅
|
|
269
|
+
```csharp
|
|
270
|
+
private static readonly SearchValues<char> s_hex = SearchValues.Create("ABCDEF");
|
|
271
|
+
int pos = text.AsSpan().IndexOfAny(s_hex);
|
|
272
|
+
```
|
|
273
|
+
**Impact: 2-10x faster for chars; 10-30x faster for multi-string (.NET 9+).**
|
|
274
|
+
|
|
275
|
+
## Detection
|
|
276
|
+
|
|
277
|
+
Scan recipes for critical anti-patterns. Run these and report exact counts of issues found in each case.
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
# .IndexOf(string) without StringComparison (culture-aware, 2-3x slower)
|
|
281
|
+
grep -rn --include='*.cs' -E '\.IndexOf\("[^"]+"\)' --exclude-dir=bin --exclude-dir=obj . | wc -l
|
|
282
|
+
|
|
283
|
+
# .Substring( calls (allocates new string — consider AsSpan)
|
|
284
|
+
grep -rn --include='*.cs' '\.Substring(' --exclude-dir=bin --exclude-dir=obj . | wc -l
|
|
285
|
+
|
|
286
|
+
# .StartsWith/.EndsWith without StringComparison (culture-aware, 2-3x slower)
|
|
287
|
+
grep -rn --include='*.cs' -E '\.(StartsWith|EndsWith)\("[^"]+"\)' --exclude-dir=bin --exclude-dir=obj . | wc -l
|
|
288
|
+
|
|
289
|
+
# .Contains(string) without StringComparison — NOTE: will also match collection .Contains() calls; filter to string receivers
|
|
290
|
+
grep -rn --include='*.cs' -E '\.Contains\("[^"]+"\)' --exclude-dir=bin --exclude-dir=obj . | wc -l
|
|
291
|
+
```
|