Skip to content

Commit bcc5bff

Browse files
author
vp
committed
add more tasks + Benchmark project
1 parent 56bdf1b commit bcc5bff

14 files changed

Lines changed: 366 additions & 37 deletions

.config/dotnet-tools.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,27 @@
4343
"dotnet-trace"
4444
],
4545
"rollForward": false
46+
},
47+
"dotnet-dump": {
48+
"version": "9.0.621003",
49+
"commands": [
50+
"dotnet-dump"
51+
],
52+
"rollForward": false
53+
},
54+
"dotnet-project-licenses": {
55+
"version": "2.7.1",
56+
"commands": [
57+
"dotnet-project-licenses"
58+
],
59+
"rollForward": false
60+
},
61+
"nuget-license": {
62+
"version": "4.0.0",
63+
"commands": [
64+
"nuget-license"
65+
],
66+
"rollForward": false
4667
}
4768
}
4869
}

.vscode/tasks-compliance.ps1

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
param(
2+
[Parameter(Mandatory=$true)][string]$Command,
3+
[string]$SolutionPath = (Join-Path $PSScriptRoot '..' 'BridgingIT.DevKit.Examples.GettingStarted.sln')
4+
)
5+
$ErrorActionPreference='Stop'
6+
7+
function Ensure-LicenseTool {
8+
Write-Host 'Restoring local dotnet tools (manifest)...' -ForegroundColor DarkCyan
9+
& dotnet tool restore | Out-Null
10+
$toolList = & dotnet tool list --local 2>&1
11+
if($LASTEXITCODE -ne 0){ throw 'Local tool manifest not found or restore failed.' }
12+
if(-not ($toolList -match 'nuget-license')){
13+
throw 'nuget-license missing from local manifest. Add with: dotnet tool install nuget-license --local'
14+
}
15+
}
16+
17+
switch($Command.ToLowerInvariant()){
18+
'licenses' {
19+
Ensure-LicenseTool
20+
$outDir = Join-Path (Join-Path $PSScriptRoot '..') '.tmp/compliance'
21+
if(-not (Test-Path $outDir)){ New-Item -ItemType Directory -Force -Path $outDir | Out-Null }
22+
$timestamp = Get-Date -Format 'yyyyMMdd_HHmmss'
23+
$mdFile = Join-Path $outDir "licenses_$timestamp.md"
24+
$jsonFile = Join-Path $outDir "licenses_$timestamp.json"
25+
Write-Host "Generating license report -> $mdFile" -ForegroundColor Cyan
26+
# Prefer nuget-license tool; generates SPDX-focused license information
27+
$nlOutput = & dotnet tool run nuget-license -i $SolutionPath -t -o JsonPretty 2>&1
28+
$parseSource = ($nlOutput -join "`n")
29+
if(-not ($parseSource.TrimStart() -match '^[\[{]')){ throw "nuget-license did not return JSON output. Raw: $parseSource" }
30+
try { $data = $parseSource | ConvertFrom-Json } catch { throw 'Failed to parse nuget-license JSON output.' }
31+
if(-not ($data -is [System.Collections.IEnumerable])){ throw 'nuget-license JSON unexpected shape (expected array).' }
32+
$rows = @('| Package | Version | License | LicenseUrl |','|---------|---------|---------|-----------|')
33+
$licenseStats = @{}
34+
$jsonList = @()
35+
foreach($pkg in $data){
36+
$name = $pkg.PackageId
37+
$ver = $pkg.PackageVersion
38+
$licRaw = $pkg.License
39+
$licUrl = $pkg.LicenseUrl
40+
if(-not $licRaw){ $licRaw = '(unknown)' }
41+
if(-not $licUrl){ $licUrl = '(none)' }
42+
# If license text is embedded (large multi-line EULA), classify
43+
$lic = if($licRaw.Length -gt 120 -or $licRaw -match "\n") { '(Embedded License Text)' } else { $licRaw }
44+
$rows += "| $name | $ver | $lic | $licUrl |"
45+
if($licenseStats.ContainsKey($lic)){ $licenseStats[$lic]++ } else { $licenseStats[$lic] = 1 }
46+
$jsonList += [pscustomobject]@{ package=$name; version=$ver; license=$lic; licenseUrl=$licUrl }
47+
}
48+
# Add summary section
49+
$total = $jsonList.Count
50+
$unknownCount = ($jsonList | Where-Object { $_.license -eq '(unknown)' }).Count
51+
$summaryLines = @("","## License Summary","Total packages: $total","Unknown licenses: $unknownCount","Top licenses:")
52+
foreach($key in ($licenseStats.Keys | Sort-Object)){
53+
$count = $licenseStats[$key]
54+
$summaryLines += " - ${key}: ${count}"
55+
}
56+
($rows + $summaryLines) -join "`n" | Set-Content -Path $mdFile -Encoding UTF8
57+
$jsonObj = [pscustomobject]@{ generated = (Get-Date).ToString('o'); total = $total; unknown = $unknownCount; licenses = $licenseStats; packages=$jsonList }
58+
$jsonObj | ConvertTo-Json -Depth 6 | Set-Content -Path $jsonFile -Encoding UTF8
59+
Write-Host 'License reports created with nuget-license:' -ForegroundColor Green
60+
Write-Host " MD: $mdFile" -ForegroundColor Green
61+
Write-Host " JSON: $jsonFile" -ForegroundColor Green
62+
}
63+
default { throw "Unknown compliance command: $Command" }
64+
}

.vscode/tasks-diagnostics.ps1

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
param(
2+
[Parameter(Mandatory=$true)][string]$Command
3+
)
4+
$ErrorActionPreference='Stop'
5+
6+
function Ensure-Tool($tool,$install){
7+
if(-not (Get-Command $tool -ErrorAction SilentlyContinue)){
8+
Write-Host "Installing missing tool: $tool" -ForegroundColor Yellow
9+
& dotnet tool install --global $install
10+
if($LASTEXITCODE -ne 0){ throw "Failed to install tool $tool" }
11+
}
12+
}
13+
14+
${script:DotNetOnly} = $false
15+
function Select-Pid($title){
16+
Import-Module PwshSpectreConsole -ErrorAction Stop
17+
$procs = Get-Process | Where-Object { $_.Id -gt 0 }
18+
if($script:DotNetOnly){ $procs = $procs | Where-Object { $_.ProcessName -match 'dotnet|Presentation.Web.Server' } }
19+
$procs = $procs | Sort-Object ProcessName,Id
20+
$rows = @()
21+
foreach($p in $procs){
22+
$label = "$($p.ProcessName) (#$($p.Id))"
23+
if($rows -notcontains $label){ $rows += $label }
24+
}
25+
if(-not $rows){ Write-Host 'No matching processes found.' -ForegroundColor Yellow; return $null }
26+
$choices = $rows + 'Cancel'
27+
$sel = Read-SpectreSelection -Title $title -Choices $choices -EnableSearch -PageSize 25
28+
Write-Host "Raw selection: '$sel'" -ForegroundColor DarkGray
29+
if([string]::IsNullOrWhiteSpace($sel) -or $sel -eq 'Cancel'){ return $null }
30+
if($sel -match '\(#(\d+)\)$'){ Write-Host "Selected PID: $($Matches[1])" -ForegroundColor DarkGray; return [int]$Matches[1] }
31+
Write-Host "Could not parse PID from selection: $sel" -ForegroundColor Yellow
32+
return $null
33+
}
34+
35+
switch($Command.ToLowerInvariant()){
36+
'bench' {
37+
# Run benchmark project if exists
38+
$benchProj = Get-ChildItem -Recurse -Filter '*Benchmarks.csproj' | Select-Object -First 1
39+
if(-not $benchProj){ Write-Host 'No benchmark project (*.Benchmarks.csproj) found.' -ForegroundColor Yellow; break }
40+
Write-Host "Attempting benchmark run: $($benchProj.FullName)" -ForegroundColor Cyan
41+
try {
42+
$env:DOTNET_EnableDiagnostics=0
43+
& dotnet run --project $benchProj.FullName -c Release -- --filter '*' --anyCategories "*"
44+
Remove-Item Env:DOTNET_EnableDiagnostics -ErrorAction SilentlyContinue
45+
if($LASTEXITCODE -ne 0){ throw 'BenchmarkDotNet failed' }
46+
Write-Host 'Benchmarks completed.' -ForegroundColor Green
47+
}
48+
catch {
49+
Write-Host "Benchmark run failed: $($_.Exception.Message). Falling back to simple performance smoke (build + run)." -ForegroundColor Yellow
50+
Remove-Item Env:DOTNET_EnableDiagnostics -ErrorAction SilentlyContinue
51+
& dotnet build $benchProj.FullName -c Release
52+
if($LASTEXITCODE -ne 0){ Write-Host 'Fallback build failed.' -ForegroundColor Red; break }
53+
& dotnet run --project $benchProj.FullName -c Release --no-build
54+
Write-Host 'Fallback benchmark smoke complete.' -ForegroundColor Green
55+
}
56+
}
57+
'trace-flame' {
58+
Ensure-Tool 'dotnet-counters' 'dotnet-counters'
59+
Ensure-Tool 'dotnet-trace' 'dotnet-trace'
60+
$script:DotNetOnly = $true
61+
$procId = Select-Pid 'Select process to trace (flame)'
62+
if(-not $procId){ Write-Host 'No PID selected.' -ForegroundColor Yellow; break }
63+
$outDir = Join-Path $PSScriptRoot '..' '.tmp' 'diagnostics'
64+
New-Item -ItemType Directory -Force -Path $outDir | Out-Null
65+
$fileBase = "trace_${procId}_$(Get-Date -Format 'yyyyMMdd_HHmmss')"
66+
$traceFile = Join-Path $outDir "$fileBase.nettrace"
67+
$speedFile = Join-Path $outDir "$fileBase.speedscope.json"
68+
Write-Host "Collecting trace for PID $procId ..." -ForegroundColor Cyan
69+
& dotnet-trace collect --process-id $procId --providers Microsoft-DotNETCore-SampleProfiler:1 --duration 00:00:10 -o $traceFile
70+
if($LASTEXITCODE -ne 0){
71+
Write-Host 'SampleProfiler provider failed, retrying with default trace config (cpu+gc)...' -ForegroundColor Yellow
72+
& dotnet-trace collect --process-id $procId --duration 00:00:10 -o $traceFile
73+
if($LASTEXITCODE -ne 0){ throw 'Trace collection failed (fallback also failed)' }
74+
}
75+
Write-Host 'Converting to speedscope...' -ForegroundColor Cyan
76+
& dotnet-trace convert --format SpeedScope $traceFile -o $speedFile
77+
if($LASTEXITCODE -ne 0){ throw 'Trace conversion failed' }
78+
Write-Host "Trace complete: $traceFile" -ForegroundColor Green
79+
Write-Host "Speedscope file: $speedFile" -ForegroundColor Green
80+
}
81+
'dump-heap' {
82+
Ensure-Tool 'dotnet-dump' 'dotnet-dump'
83+
$script:DotNetOnly = $true
84+
$procId = Select-Pid 'Select process for heap dump'
85+
if(-not $procId){ Write-Host 'No PID selected.' -ForegroundColor Yellow; break }
86+
$outDir = Join-Path $PSScriptRoot '..' '.tmp' 'diagnostics'
87+
New-Item -ItemType Directory -Force -Path $outDir | Out-Null
88+
$dumpFile = Join-Path $outDir "heap_${procId}_$(Get-Date -Format 'yyyyMMdd_HHmmss').dmp"
89+
Write-Host "Creating heap dump for PID $procId ..." -ForegroundColor Cyan
90+
& dotnet-dump collect --process-id $procId --type full -o $dumpFile
91+
if($LASTEXITCODE -ne 0){ throw 'Heap dump failed' }
92+
Write-Host "Heap dump created: $dumpFile" -ForegroundColor Green
93+
}
94+
'gc-stats' { # Collect GC stats via dotnet-counters https://learn.microsoft.com/en-us/aspnet/core/log-mon/metrics/metrics?view=aspnetcore-9.0#view-metrics-with-dotnet-counters
95+
Ensure-Tool 'dotnet-counters' 'dotnet-counters'
96+
$script:DotNetOnly = $true
97+
$procId = Select-Pid 'Select process for GC stats'
98+
if(-not $procId){ Write-Host 'No PID selected.' -ForegroundColor Yellow; break }
99+
Write-Host "Sampling GC counters for PID $procId (5s) ..." -ForegroundColor Cyan
100+
# & dotnet-counters monitor --process-id $procId --counters "System.Runtime[gc-heap-size;time-in-gc]" --refresh-interval 1 --duration 5
101+
& dotnet-counters monitor --process-id $procId --counters "System.Runtime" --refresh-interval 1 --duration 5
102+
if($LASTEXITCODE -ne 0){ throw 'GC stats collection failed' }
103+
Write-Host 'GC sampling complete.' -ForegroundColor Green
104+
}
105+
'aspnet-metrics' {
106+
Ensure-Tool 'dotnet-counters' 'dotnet-counters'
107+
$script:DotNetOnly = $true
108+
$procId = Select-Pid 'Select ASP.NET Core process for metrics'
109+
if(-not $procId){ Write-Host 'No PID selected.' -ForegroundColor Yellow; break }
110+
Write-Host "Monitoring ASP.NET Core + runtime counters for PID $procId (10s) ..." -ForegroundColor Cyan
111+
# $counterGroups = @(
112+
# 'Microsoft.AspNetCore.Hosting[requests-started;requests-completed;current-requests]',
113+
# 'Microsoft.AspNetCore.Server.Kestrel[connection-queue-length;connections-active;connections-opened;connections-closed]',
114+
# 'System.Net.Http[requests-started;requests-failed]',
115+
# 'System.Runtime[cpu-usage;working-set;gc-heap-size;gen-0-gc-count;gen-1-gc-count;gen-2-gc-count;time-in-gc]'
116+
# )
117+
$counterGroups = @(
118+
'Microsoft.AspNetCore.Hosting'
119+
)
120+
$countersArg = $counterGroups -join ' '
121+
& dotnet-counters monitor --process-id $procId --counters $countersArg --refresh-interval 1 --duration 10
122+
if($LASTEXITCODE -ne 0){ throw 'ASP.NET metrics collection failed' }
123+
Write-Host 'ASP.NET metrics sampling complete.' -ForegroundColor Green
124+
}
125+
default { throw "Unknown diagnostics command: $Command" }
126+
}

.vscode/tasks-dotnet.ps1

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,24 @@ switch ($Command.ToLowerInvariant()) {
2626
'format-check' { Invoke-Dotnet @('format',$SolutionPath,'--verify-no-changes') }
2727
'format-apply' { Invoke-Dotnet @('format',$SolutionPath) }
2828
'vulnerabilities' { Invoke-Dotnet @('list',$SolutionPath,'package','--vulnerable') }
29+
'vulnerabilities-deep' { Invoke-Dotnet @('list',$SolutionPath,'package','--vulnerable','--include-transitive') }
2930
'outdated' { Invoke-Dotnet @('list',$SolutionPath,'package','--outdated') }
31+
'outdated-json' {
32+
$tmpDir = Join-Path (Join-Path $PSScriptRoot '..') '.tmp/compliance'
33+
if(-not (Test-Path $tmpDir)){ New-Item -ItemType Directory -Force -Path $tmpDir | Out-Null }
34+
$outFile = Join-Path $tmpDir ("outdated_" + (Get-Date -Format 'yyyyMMdd_HHmmss') + '.json')
35+
Write-Host "Collecting outdated packages (JSON) -> $outFile" -ForegroundColor Cyan
36+
# dotnet list outdated does not have native JSON; capture text then transform rudimentary structure
37+
$raw = & dotnet list $SolutionPath package --outdated
38+
if($LASTEXITCODE -ne 0){ throw 'dotnet list outdated failed' }
39+
$lines = $raw -split "`r?`n"
40+
$pkgs = @()
41+
foreach($l in $lines){
42+
if($l -match '>(\s*)(?<name>[^\s]+)\s+(?<current>\S+)\s+(?<wanted>\S+)\s+(?<latest>\S+)'){ $pkgs += [pscustomobject]@{ name=$Matches.name; current=$Matches.current; wanted=$Matches.wanted; latest=$Matches.latest } }
43+
}
44+
$pkgs | ConvertTo-Json -Depth 4 | Set-Content -Path $outFile -Encoding UTF8
45+
Write-Host ("Outdated packages captured: {0}" -f $pkgs.Count) -ForegroundColor Green
46+
}
3047
'analyzers' { Invoke-Dotnet @('build',$SolutionPath,'-warnaserror','/p:RunAnalyzers=true','/p:EnableNETAnalyzers=true','/p:AnalysisLevel=latest') }
3148
'project-build' { if (-not $ProjectPath) { throw 'ProjectPath required for project-build' }; Invoke-Dotnet (@('build',$ProjectPath) + $logArgs) }
3249
'project-publish' { if (-not $ProjectPath) { throw 'ProjectPath required for project-publish' }; Invoke-Dotnet (@('publish',$ProjectPath) + $logArgs) }

Directory.Packages.props

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@
3131
<PackageVersion Include="Microsoft.VisualStudio.DiagnosticsHub.BenchmarkDotNetDiagnosers" Version="18.0.36313.1" />
3232
<PackageVersion Include="NetArchTest.eNhancedEdition" Version="1.4.5" />
3333
<PackageVersion Include="NSubstitute" Version="5.3.0" />
34+
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.13.1" />
35+
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.13.1" />
36+
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.13.0" />
37+
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.13.0" />
3438
<PackageVersion Include="Scalar.AspNetCore" Version="2.9.0" />
3539
<PackageVersion Include="Serilog.Enrichers.Environment" Version="3.0.1" />
3640
<PackageVersion Include="Serilog.Enrichers.ShortTypeName" Version="1.1.0" />

docker-compose.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,18 @@ services:
5454
networks:
5555
- bdk_gettingstarted
5656

57+
aspire-dashboard:
58+
image: mcr.microsoft.com/dotnet/aspire-dashboard:latest
59+
container_name: aspire-dashboard
60+
restart: unless-stopped
61+
ports:
62+
- 18889:18888
63+
- 43179:18889
64+
environment:
65+
- DOTNET_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS=true
66+
networks:
67+
- bdk_gettingstarted
68+
5769
volumes:
5870
mssql:
5971
name: bdk_gettingstarted_mssql

src/Presentation.Web.Server/Presentation.Web.Server.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@
3030
</PackageReference>
3131
<PackageReference Include="Microsoft.Extensions.ApiDescription.Server" />
3232
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" />
33+
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" />
34+
<PackageReference Include="OpenTelemetry.Extensions.Hosting" />
35+
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" />
36+
<PackageReference Include="OpenTelemetry.Instrumentation.Http" />
3337
<PackageReference Include="Scalar.AspNetCore" />
3438
<PackageReference Include="Serilog.Enrichers.Environment" />
3539
<PackageReference Include="Serilog.Enrichers.ShortTypeName" />

src/Presentation.Web.Server/Program.cs

Lines changed: 6 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,6 @@
88
using BridgingIT.DevKit.Presentation;
99
using BridgingIT.DevKit.Presentation.Web;
1010
using Hellang.Middleware.ProblemDetails;
11-
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
12-
using Microsoft.Extensions.Diagnostics.HealthChecks;
13-
using Microsoft.Extensions.Hosting;
14-
using System;
1511

1612
// ===============================================================================================
1713
// Configure the host
@@ -66,12 +62,11 @@
6662

6763
// ===============================================================================================
6864
// Configure Health Checks
69-
builder.Services.AddHealthChecks()
70-
// readiness checks
71-
.AddCheck("self", () => HealthCheckResult.Healthy()); // liveness
72-
// .AddSqlServer(builder.Configuration.GetConnectionString("Default"),
73-
// name: "sql", failureStatus: HealthStatus.Unhealthy, timeout: TimeSpan.FromSeconds(2))
74-
// .AddRedis(builder.Configuration.GetConnectionString("Redis"), "redis");
65+
builder.Services.AddHealthChecks(builder.Configuration);
66+
67+
// ===============================================================================================
68+
// Configure Observability
69+
builder.Services.AddOpenTelemetry(builder.Configuration);
7570

7671
// ===============================================================================================
7772
// Configure the HTTP request pipeline
@@ -97,26 +92,11 @@
9792

9893
app.UseCurrentUserLogging();
9994

95+
app.MapHealthChecks();
10096
app.MapModules();
10197
app.MapControllers();
10298
app.MapEndpoints();
10399

104-
// Liveness: only confirms the app is running
105-
app.MapHealthChecks("/health/live", new HealthCheckOptions
106-
{
107-
Predicate = r => r.Name == "self",
108-
//ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
109-
});
110-
111-
// Readiness: checks all except "self" or vice-versa depending on your naming
112-
app.MapHealthChecks("/health/ready", new HealthCheckOptions
113-
{
114-
Predicate = r => r.Name != "self",
115-
//ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
116-
});
117-
118-
app.MapHealthChecks("/health");
119-
120100
app.Run();
121101

122102
namespace BridgingIT.DevKit.Examples.GettingStarted.Presentation.Web.Server

0 commit comments

Comments
 (0)