From 8776a7fddf664ed35451459e2f50aba8ce23dc51 Mon Sep 17 00:00:00 2001 From: Karol Zadora-Przylecki Date: Wed, 11 Feb 2026 10:53:05 -0800 Subject: [PATCH 1/5] Add a script for startup performance measurement (#14345) * Add startup perf collection script * Analyze trace more efficiently * Increase pause between iterations * Fix TraceAnalyzer * Add startup-perf skill --- .github/skills/startup-perf/SKILL.md | 193 +++++ AGENTS.md | 1 + docs/getting-perf-traces.md | 10 +- tools/perf/Measure-StartupPerformance.ps1 | 678 ++++++++++++++++++ tools/perf/TraceAnalyzer/Program.cs | 80 +++ tools/perf/TraceAnalyzer/TraceAnalyzer.csproj | 16 + 6 files changed, 977 insertions(+), 1 deletion(-) create mode 100644 .github/skills/startup-perf/SKILL.md create mode 100644 tools/perf/Measure-StartupPerformance.ps1 create mode 100644 tools/perf/TraceAnalyzer/Program.cs create mode 100644 tools/perf/TraceAnalyzer/TraceAnalyzer.csproj diff --git a/.github/skills/startup-perf/SKILL.md b/.github/skills/startup-perf/SKILL.md new file mode 100644 index 00000000000..33ca4d3875f --- /dev/null +++ b/.github/skills/startup-perf/SKILL.md @@ -0,0 +1,193 @@ +--- +name: startup-perf +description: Measures Aspire application startup performance using dotnet-trace and the TraceAnalyzer tool. Use this when asked to measure impact of a code change on Aspire application startup performance. +--- + +# Aspire Startup Performance Measurement + +This skill provides patterns and practices for measuring .NET Aspire application startup performance using the `Measure-StartupPerformance.ps1` script and the companion `TraceAnalyzer` tool. + +## Overview + +The startup performance tooling collects `dotnet-trace` traces from an Aspire AppHost application and computes the startup duration from `AspireEventSource` events. Specifically, it measures the time between the `DcpModelCreationStart` (event ID 17) and `DcpModelCreationStop` (event ID 18) events emitted by the `Microsoft-Aspire-Hosting` EventSource provider. + +**Script Location**: `tools/perf/Measure-StartupPerformance.ps1` +**TraceAnalyzer Location**: `tools/perf/TraceAnalyzer/` +**Documentation**: `docs/getting-perf-traces.md` + +## Prerequisites + +- PowerShell 7+ +- `dotnet-trace` global tool (`dotnet tool install -g dotnet-trace`) +- .NET SDK (restored via `./restore.cmd` or `./restore.sh`) + +## Quick Start + +### Single Measurement + +```powershell +# From repository root — measures the default TestShop.AppHost +.\tools\perf\Measure-StartupPerformance.ps1 +``` + +### Multiple Iterations with Statistics + +```powershell +.\tools\perf\Measure-StartupPerformance.ps1 -Iterations 5 +``` + +### Custom Project + +```powershell +.\tools\perf\Measure-StartupPerformance.ps1 -ProjectPath "path\to\MyApp.AppHost.csproj" -Iterations 3 +``` + +### Preserve Traces for Manual Analysis + +```powershell +.\tools\perf\Measure-StartupPerformance.ps1 -Iterations 3 -PreserveTraces -TraceOutputDirectory "C:\traces" +``` + +### Verbose Output + +```powershell +.\tools\perf\Measure-StartupPerformance.ps1 -Verbose +``` + +## Parameters + +| Parameter | Default | Description | +|-----------|---------|-------------| +| `ProjectPath` | TestShop.AppHost | Path to the AppHost `.csproj` to measure | +| `Iterations` | 1 | Number of measurement runs (1–100) | +| `PreserveTraces` | `$false` | Keep `.nettrace` files after analysis | +| `TraceOutputDirectory` | temp folder | Directory for preserved trace files | +| `SkipBuild` | `$false` | Skip `dotnet build` before running | +| `TraceDurationSeconds` | 60 | Maximum trace collection time (1–86400) | +| `PauseBetweenIterationsSeconds` | 45 | Pause between iterations (0–3600) | +| `Verbose` | `$false` | Show detailed output | + +## How It Works + +The script follows this sequence: + +1. **Prerequisites check** — Verifies `dotnet-trace` is installed and the project exists. +2. **Build** — Builds the AppHost project in Release configuration (unless `-SkipBuild`). +3. **Build TraceAnalyzer** — Builds the companion `tools/perf/TraceAnalyzer` project. +4. **For each iteration:** + a. Locates the compiled executable (Arcade-style or traditional output paths). + b. Reads `launchSettings.json` for environment variables. + c. Launches the AppHost as a separate process. + d. Attaches `dotnet-trace` to the running process with the `Microsoft-Aspire-Hosting` provider. + e. Waits for the trace to complete (duration timeout or process exit). + f. Runs the TraceAnalyzer to extract the startup duration from the `.nettrace` file. + g. Cleans up processes. +5. **Reports results** — Prints per-iteration times and statistics (min, max, average, std dev). + +## TraceAnalyzer Tool + +The `tools/perf/TraceAnalyzer` is a small .NET console app that parses `.nettrace` files using the `Microsoft.Diagnostics.Tracing.TraceEvent` library. + +### What It Does + +- Opens the `.nettrace` file with `EventPipeEventSource` +- Listens for events from the `Microsoft-Aspire-Hosting` provider +- Extracts timestamps for `DcpModelCreationStart` (ID 17) and `DcpModelCreationStop` (ID 18) +- Outputs the duration in milliseconds (or `"null"` if events are not found) + +### Standalone Usage + +```bash +dotnet run --project tools/perf/TraceAnalyzer -c Release -- +``` + +## Understanding Output + +### Successful Run + +``` +================================================== + Aspire Startup Performance Measurement +================================================== + +Project: TestShop.AppHost +Iterations: 3 +... + +Iteration 1 +---------------------------------------- +Starting TestShop.AppHost... +Attaching trace collection to PID 12345... +Collecting performance trace... +Trace collection completed. +Analyzing trace: ... +Startup time: 1234.56 ms + +... + +================================================== + Results Summary +================================================== + +Iteration StartupTimeMs +--------- ------------- + 1 1234.56 + 2 1189.23 + 3 1201.45 + +Statistics: + Successful iterations: 3 / 3 + Minimum: 1189.23 ms + Maximum: 1234.56 ms + Average: 1208.41 ms + Std Dev: 18.92 ms +``` + +### Common Issues + +| Symptom | Cause | Fix | +|---------|-------|-----| +| `dotnet-trace is not installed` | Missing global tool | Run `dotnet tool install -g dotnet-trace` | +| `Could not find compiled executable` | Project not built | Remove `-SkipBuild` or build manually | +| `Could not find DcpModelCreation events` | Trace too short or events not emitted | Increase `-TraceDurationSeconds` | +| `Application exited immediately` | App crash on startup | Check app logs, ensure dependencies are available | +| `dotnet-trace exited with code != 0` | Trace collection error | Check verbose output; trace file may still be valid | + +## Comparing Before/After Performance + +To measure the impact of a code change: + +```powershell +# 1. Measure baseline (on main branch) +git checkout main +.\tools\perf\Measure-StartupPerformance.ps1 -Iterations 5 -PreserveTraces -TraceOutputDirectory "C:\traces\baseline" + +# 2. Measure with changes +git checkout my-feature-branch +.\tools\perf\Measure-StartupPerformance.ps1 -Iterations 5 -PreserveTraces -TraceOutputDirectory "C:\traces\feature" + +# 3. Compare the reported averages and std devs +``` + +Use enough iterations (5+) and a consistent pause between iterations for reliable comparisons. + +## Collecting Traces for Manual Analysis + +If you need to inspect trace files manually (e.g., in PerfView or Visual Studio): + +```powershell +.\tools\perf\Measure-StartupPerformance.ps1 -PreserveTraces -TraceOutputDirectory "C:\my-traces" +``` + +See `docs/getting-perf-traces.md` for guidance on analyzing traces with PerfView or `dotnet trace report`. + +## EventSource Provider Details + +The `Microsoft-Aspire-Hosting` EventSource emits events for key Aspire lifecycle milestones. The startup performance script focuses on: + +| Event ID | Event Name | Description | +|----------|------------|-------------| +| 17 | `DcpModelCreationStart` | Marks the beginning of DCP model creation | +| 18 | `DcpModelCreationStop` | Marks the completion of DCP model creation | + +The measured startup time is the wall-clock difference between these two events, representing the time to create all application services and supporting dependencies. diff --git a/AGENTS.md b/AGENTS.md index cb4d5711eb1..cb6596c3d31 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -355,6 +355,7 @@ The following specialized skills are available in `.github/skills/`: - **test-management**: Quarantines or disables flaky/problematic tests using the QuarantineTools utility - **connection-properties**: Expert for creating and improving Connection Properties in Aspire resources - **dependency-update**: Guides dependency version updates by checking nuget.org, triggering the dotnet-migrate-package Azure DevOps pipeline, and monitoring runs +- **startup-perf**: Measures Aspire application startup performance using dotnet-trace and the TraceAnalyzer tool ## Pattern-Based Instructions diff --git a/docs/getting-perf-traces.md b/docs/getting-perf-traces.md index a669c591ee0..94a5a14a0d5 100644 --- a/docs/getting-perf-traces.md +++ b/docs/getting-perf-traces.md @@ -28,8 +28,16 @@ Once you are ready, hit "Start Collection" button and run your scenario. When done with the scenario, hit "Stop Collection". Wait for PerfView to finish merging and analyzing data (the "working" status bar stops flashing). -### Verify that the trace contains Aspire data +### Verify that PerfView trace contains Aspire data This is an optional step, but if you are wondering if your trace has been captured properly, you can check the following: 1. Open the trace (usually named PerfViewData.etl, if you haven't changed the name) and double click Events view. Verify you have a bunch of events from the Microsoft-Aspire-Hosting provider. + +## Profiling scripts + +The `tools/perf` folder in the repository contains scripts that help quickly assess the impact of code changes on key performance scenarios. Currently available scripts are: + +| Script | Description | +| --- | --------- | +| `Measure-StartupPerformance.ps1` | Measures startup time for a specific Aspire project. More specifically, the script measures the time to get all application services and supporting dependencies CREATED; the application is not necessarily responsive after measured time. | diff --git a/tools/perf/Measure-StartupPerformance.ps1 b/tools/perf/Measure-StartupPerformance.ps1 new file mode 100644 index 00000000000..626adff10ed --- /dev/null +++ b/tools/perf/Measure-StartupPerformance.ps1 @@ -0,0 +1,678 @@ +<# +.SYNOPSIS + Measures .NET Aspire application startup performance by collecting ETW traces. + +.DESCRIPTION + This script runs an Aspire application, collects a performance trace + using dotnet-trace, and computes the startup time from AspireEventSource events. + The trace collection ends when the DcpModelCreationStop event is fired. + +.PARAMETER ProjectPath + Path to the AppHost project (.csproj) to measure. Can be absolute or relative. + Defaults to the TestShop.AppHost project in the playground folder. + +.PARAMETER Iterations + Number of times to run the scenario and collect traces. Defaults to 1. + +.PARAMETER PreserveTraces + If specified, trace files are preserved after the run. By default, traces are + stored in a temporary folder and deleted after analysis. + +.PARAMETER TraceOutputDirectory + Directory where trace files will be saved when PreserveTraces is set. + Defaults to a 'traces' subdirectory in the script folder. + +.PARAMETER SkipBuild + If specified, skips building the project before running. + +.PARAMETER TraceDurationSeconds + Duration in seconds for the trace collection. Defaults to 60 (1 minute). + The value is automatically converted to the dd:hh:mm:ss format required by dotnet-trace. + +.PARAMETER PauseBetweenIterationsSeconds + Number of seconds to pause between iterations. Defaults to 15. + Set to 0 to disable the pause. + +.PARAMETER Verbose + If specified, shows detailed output during execution. + +.EXAMPLE + .\Measure-StartupPerformance.ps1 + +.EXAMPLE + .\Measure-StartupPerformance.ps1 -Iterations 5 + +.EXAMPLE + .\Measure-StartupPerformance.ps1 -ProjectPath "C:\MyApp\MyApp.AppHost.csproj" -Iterations 3 + +.EXAMPLE + .\Measure-StartupPerformance.ps1 -Iterations 3 -PreserveTraces -TraceOutputDirectory "C:\traces" + +.EXAMPLE + .\Measure-StartupPerformance.ps1 -TraceDurationSeconds 120 + +.EXAMPLE + .\Measure-StartupPerformance.ps1 -Iterations 5 -PauseBetweenIterationsSeconds 30 + +.NOTES + Requires: + - PowerShell 7+ + - dotnet-trace global tool (dotnet tool install -g dotnet-trace) + - .NET SDK +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory = $false)] + [string]$ProjectPath, + + [Parameter(Mandatory = $false)] + [ValidateRange(1, 100)] + [int]$Iterations = 1, + + [Parameter(Mandatory = $false)] + [switch]$PreserveTraces, + + [Parameter(Mandatory = $false)] + [string]$TraceOutputDirectory, + + [Parameter(Mandatory = $false)] + [switch]$SkipBuild, + + [Parameter(Mandatory = $false)] + [ValidateRange(1, 86400)] + [int]$TraceDurationSeconds = 60, + + [Parameter(Mandatory = $false)] + [ValidateRange(0, 3600)] + [int]$PauseBetweenIterationsSeconds = 45 +) + +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version Latest + +# Constants +$EventSourceName = 'Microsoft-Aspire-Hosting' +$DcpModelCreationStartEventId = 17 +$DcpModelCreationStopEventId = 18 + +# Get repository root (script is in tools/perf) +$ScriptDir = $PSScriptRoot +$RepoRoot = (Resolve-Path (Join-Path $ScriptDir '..' '..')).Path + +# Resolve project path +if (-not $ProjectPath) { + # Default to TestShop.AppHost + $ProjectPath = Join-Path $RepoRoot 'playground' 'TestShop' 'TestShop.AppHost' 'TestShop.AppHost.csproj' +} +elseif (-not [System.IO.Path]::IsPathRooted($ProjectPath)) { + # Relative path - resolve from current directory + $ProjectPath = (Resolve-Path $ProjectPath -ErrorAction Stop).Path +} + +$AppHostProject = $ProjectPath +$AppHostDir = Split-Path $AppHostProject -Parent +$AppHostName = [System.IO.Path]::GetFileNameWithoutExtension($AppHostProject) + +# Determine output directory for traces - always use temp directory unless explicitly specified +if ($TraceOutputDirectory) { + $OutputDirectory = $TraceOutputDirectory +} +else { + # Always use a temp directory for traces + $OutputDirectory = Join-Path ([System.IO.Path]::GetTempPath()) "aspire-perf-$([System.Guid]::NewGuid().ToString('N').Substring(0, 8))" +} + +# Only delete temp directory if not preserving traces and no custom directory was specified +$ShouldCleanupDirectory = -not $PreserveTraces -and -not $TraceOutputDirectory + +# Ensure output directory exists +if (-not (Test-Path $OutputDirectory)) { + New-Item -ItemType Directory -Path $OutputDirectory -Force | Out-Null +} + +# Verify prerequisites +function Test-Prerequisites { + Write-Host "Checking prerequisites..." -ForegroundColor Cyan + + # Check dotnet-trace is installed + $dotnetTrace = Get-Command 'dotnet-trace' -ErrorAction SilentlyContinue + if (-not $dotnetTrace) { + throw "dotnet-trace is not installed. Install it with: dotnet tool install -g dotnet-trace" + } + Write-Verbose "dotnet-trace found at: $($dotnetTrace.Source)" + + # Check project exists + if (-not (Test-Path $AppHostProject)) { + throw "AppHost project not found at: $AppHostProject" + } + Write-Verbose "AppHost project found at: $AppHostProject" + + Write-Host "Prerequisites check passed." -ForegroundColor Green +} + +# Build the project +function Build-AppHost { + Write-Host "Building $AppHostName..." -ForegroundColor Cyan + + Push-Location $AppHostDir + try { + $buildOutput = & dotnet build -c Release --nologo 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Host ($buildOutput -join "`n") -ForegroundColor Red + throw "Failed to build $AppHostName" + } + Write-Verbose ($buildOutput -join "`n") + Write-Host "Build completed successfully." -ForegroundColor Green + } + finally { + Pop-Location + } +} + +# Run a single iteration of the performance test +function Invoke-PerformanceIteration { + param( + [int]$IterationNumber, + [string]$TraceOutputPath + ) + + Write-Host "`nIteration $IterationNumber" -ForegroundColor Yellow + Write-Host ("-" * 40) -ForegroundColor Yellow + + $nettracePath = "$TraceOutputPath.nettrace" + $appProcess = $null + $traceProcess = $null + + try { + # Find the compiled executable - we need the path to launch it + $exePath = $null + $dllPath = $null + + # Search in multiple possible output locations: + # 1. Arcade-style: artifacts/bin//Release// + # 2. Traditional: /bin/Release// + $searchPaths = @( + (Join-Path $RepoRoot 'artifacts' 'bin' $AppHostName 'Release'), + (Join-Path $AppHostDir 'bin' 'Release') + ) + + foreach ($basePath in $searchPaths) { + if (-not (Test-Path $basePath)) { + continue + } + + # Find TFM subdirectories (e.g., net8.0, net9.0, net10.0) + $tfmDirs = Get-ChildItem -Path $basePath -Directory -Filter 'net*' -ErrorAction SilentlyContinue + foreach ($tfmDir in $tfmDirs) { + $candidateExe = Join-Path $tfmDir.FullName "$AppHostName.exe" + $candidateDll = Join-Path $tfmDir.FullName "$AppHostName.dll" + + if (Test-Path $candidateExe) { + $exePath = $candidateExe + Write-Verbose "Found executable at: $exePath" + break + } + elseif (Test-Path $candidateDll) { + $dllPath = $candidateDll + Write-Verbose "Found DLL at: $dllPath" + break + } + } + + if ($exePath -or $dllPath) { + break + } + } + + if (-not $exePath -and -not $dllPath) { + $searchedPaths = $searchPaths -join "`n - " + throw "Could not find compiled executable or DLL. Searched in:`n - $searchedPaths`nPlease build the project first (without -SkipBuild)." + } + + # Read launchSettings.json to get environment variables + $launchSettingsPath = Join-Path $AppHostDir 'Properties' 'launchSettings.json' + $envVars = @{} + if (Test-Path $launchSettingsPath) { + Write-Verbose "Reading launch settings from: $launchSettingsPath" + try { + # Read the file and remove JSON comments (// style) before parsing + # Only remove lines that start with // (after optional whitespace) to avoid breaking URLs like https:// + $jsonLines = Get-Content $launchSettingsPath + $filteredLines = $jsonLines | Where-Object { $_.Trim() -notmatch '^//' } + $jsonContent = $filteredLines -join "`n" + $launchSettings = $jsonContent | ConvertFrom-Json + + # Try to find a suitable profile (prefer 'http' for simplicity, then first available) + $profile = $null + if ($launchSettings.profiles.http) { + $profile = $launchSettings.profiles.http + Write-Verbose "Using 'http' launch profile" + } + elseif ($launchSettings.profiles.https) { + $profile = $launchSettings.profiles.https + Write-Verbose "Using 'https' launch profile" + } + else { + # Use first profile that has environmentVariables + foreach ($prop in $launchSettings.profiles.PSObject.Properties) { + if ($prop.Value.environmentVariables) { + $profile = $prop.Value + Write-Verbose "Using '$($prop.Name)' launch profile" + break + } + } + } + + if ($profile -and $profile.environmentVariables) { + foreach ($prop in $profile.environmentVariables.PSObject.Properties) { + $envVars[$prop.Name] = $prop.Value + Write-Verbose " Environment: $($prop.Name)=$($prop.Value)" + } + } + + # Use applicationUrl to set ASPNETCORE_URLS if not already set + if ($profile -and $profile.applicationUrl -and -not $envVars.ContainsKey('ASPNETCORE_URLS')) { + $envVars['ASPNETCORE_URLS'] = $profile.applicationUrl + Write-Verbose " Environment: ASPNETCORE_URLS=$($profile.applicationUrl) (from applicationUrl)" + } + } + catch { + Write-Warning "Failed to parse launchSettings.json: $_" + } + } + else { + Write-Verbose "No launchSettings.json found at: $launchSettingsPath" + } + + # Always ensure Development environment is set + if (-not $envVars.ContainsKey('DOTNET_ENVIRONMENT')) { + $envVars['DOTNET_ENVIRONMENT'] = 'Development' + } + if (-not $envVars.ContainsKey('ASPNETCORE_ENVIRONMENT')) { + $envVars['ASPNETCORE_ENVIRONMENT'] = 'Development' + } + + # Start the AppHost application as a separate process + Write-Host "Starting $AppHostName..." -ForegroundColor Cyan + + $appPsi = [System.Diagnostics.ProcessStartInfo]::new() + if ($exePath) { + $appPsi.FileName = $exePath + $appPsi.Arguments = '' + } + else { + $appPsi.FileName = 'dotnet' + $appPsi.Arguments = "`"$dllPath`"" + } + $appPsi.WorkingDirectory = $AppHostDir + $appPsi.UseShellExecute = $false + $appPsi.RedirectStandardOutput = $true + $appPsi.RedirectStandardError = $true + $appPsi.CreateNoWindow = $true + + # Set environment variables from launchSettings.json + foreach ($key in $envVars.Keys) { + $appPsi.Environment[$key] = $envVars[$key] + } + + $appProcess = [System.Diagnostics.Process]::Start($appPsi) + $appPid = $appProcess.Id + + Write-Verbose "$AppHostName started with PID: $appPid" + + # Give the process a moment to initialize before attaching + Start-Sleep -Milliseconds 200 + + # Verify the process is still running + if ($appProcess.HasExited) { + $stdout = $appProcess.StandardOutput.ReadToEnd() + $stderr = $appProcess.StandardError.ReadToEnd() + throw "Application exited immediately with code $($appProcess.ExitCode).`nStdOut: $stdout`nStdErr: $stderr" + } + + # Start dotnet-trace to attach to the running process + Write-Host "Attaching trace collection to PID $appPid..." -ForegroundColor Cyan + + # Use dotnet-trace with the EventSource provider + # Format: ProviderName:Keywords:Level + # Keywords=0xFFFFFFFF (all), Level=5 (Verbose) + $providers = "${EventSourceName}" + + # Convert TraceDurationSeconds to dd:hh:mm:ss format required by dotnet-trace + $days = [math]::Floor($TraceDurationSeconds / 86400) + $hours = [math]::Floor(($TraceDurationSeconds % 86400) / 3600) + $minutes = [math]::Floor(($TraceDurationSeconds % 3600) / 60) + $seconds = $TraceDurationSeconds % 60 + $traceDuration = '{0:00}:{1:00}:{2:00}:{3:00}' -f $days, $hours, $minutes, $seconds + + $traceArgs = @( + 'collect', + '--process-id', $appPid, + '--providers', $providers, + '--output', $nettracePath, + '--format', 'nettrace', + '--duration', $traceDuration, + '--buffersize', '8192' + ) + + Write-Verbose "dotnet-trace arguments: $($traceArgs -join ' ')" + + $tracePsi = [System.Diagnostics.ProcessStartInfo]::new() + $tracePsi.FileName = 'dotnet-trace' + $tracePsi.Arguments = $traceArgs -join ' ' + $tracePsi.WorkingDirectory = $AppHostDir + $tracePsi.UseShellExecute = $false + $tracePsi.RedirectStandardOutput = $true + $tracePsi.RedirectStandardError = $true + $tracePsi.CreateNoWindow = $true + + $traceProcess = [System.Diagnostics.Process]::Start($tracePsi) + + Write-Host "Collecting performance trace..." -ForegroundColor Cyan + + # Wait for trace to complete + $traceProcess.WaitForExit() + + # Read app process output (what was captured while trace was running) + # Use async read to avoid blocking - read whatever is available + $appStdout = "" + $appStderr = "" + if ($appProcess -and -not $appProcess.HasExited) { + # Process is still running, we can try to read available output + # Note: ReadToEnd would block, so we read what's available after stopping + } + + $traceOutput = $traceProcess.StandardOutput.ReadToEnd() + $traceError = $traceProcess.StandardError.ReadToEnd() + + if ($traceOutput) { Write-Verbose "dotnet-trace output: $traceOutput" } + if ($traceError) { Write-Verbose "dotnet-trace stderr: $traceError" } + + # Check if trace file was created despite any errors + # dotnet-trace may report errors during cleanup but the trace file is often still valid + if ($traceProcess.ExitCode -ne 0) { + if (Test-Path $nettracePath) { + Write-Warning "dotnet-trace exited with code $($traceProcess.ExitCode), but trace file was created. Attempting to analyze." + } + else { + Write-Warning "dotnet-trace exited with code $($traceProcess.ExitCode) and no trace file was created." + return $null + } + } + + Write-Host "Trace collection completed." -ForegroundColor Green + + return $nettracePath + } + finally { + # Clean up the application process and capture its output + if ($appProcess) { + # Read any remaining output before killing the process + $appStdout = "" + $appStderr = "" + try { + # Give a moment for any buffered output + Start-Sleep -Milliseconds 100 + + # We need to read asynchronously since the process may still be running + # Read what's available without blocking indefinitely + $stdoutTask = $appProcess.StandardOutput.ReadToEndAsync() + $stderrTask = $appProcess.StandardError.ReadToEndAsync() + + # Wait briefly for output + [System.Threading.Tasks.Task]::WaitAll(@($stdoutTask, $stderrTask), 1000) | Out-Null + + if ($stdoutTask.IsCompleted) { + $appStdout = $stdoutTask.Result + } + if ($stderrTask.IsCompleted) { + $appStderr = $stderrTask.Result + } + } + catch { + # Ignore errors reading output + } + + if ($appStdout) { + Write-Verbose "Application stdout:`n$appStdout" + } + if ($appStderr) { + Write-Verbose "Application stderr:`n$appStderr" + } + + if (-not $appProcess.HasExited) { + Write-Verbose "Stopping $AppHostName (PID: $($appProcess.Id))..." + try { + # Try graceful shutdown first + $appProcess.Kill($true) + $appProcess.WaitForExit(5000) | Out-Null + } + catch { + Write-Warning "Failed to stop application: $_" + } + } + $appProcess.Dispose() + } + + # Clean up trace process + if ($traceProcess) { + if (-not $traceProcess.HasExited) { + try { + $traceProcess.Kill() + $traceProcess.WaitForExit(2000) | Out-Null + } + catch { + # Ignore errors killing trace process + } + } + $traceProcess.Dispose() + } + } +} + +# Path to the trace analyzer tool +$TraceAnalyzerDir = Join-Path $ScriptDir 'TraceAnalyzer' +$TraceAnalyzerProject = Join-Path $TraceAnalyzerDir 'TraceAnalyzer.csproj' + +# Build the trace analyzer tool +function Build-TraceAnalyzer { + if (-not (Test-Path $TraceAnalyzerProject)) { + Write-Warning "TraceAnalyzer project not found at: $TraceAnalyzerProject" + return $false + } + + Write-Verbose "Building TraceAnalyzer tool..." + $buildOutput = & dotnet build $TraceAnalyzerProject -c Release --verbosity quiet 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Warning "Failed to build TraceAnalyzer: $buildOutput" + return $false + } + + Write-Verbose "TraceAnalyzer built successfully" + return $true +} + +# Parse nettrace file using the TraceAnalyzer tool +function Get-StartupTiming { + param( + [string]$TracePath + ) + + Write-Host "Analyzing trace: $TracePath" -ForegroundColor Cyan + + if (-not (Test-Path $TracePath)) { + Write-Warning "Trace file not found: $TracePath" + return $null + } + + try { + $output = & dotnet run --project $TraceAnalyzerProject -c Release --no-build -- $TracePath 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Warning "TraceAnalyzer failed: $output" + return $null + } + + $result = $output | Select-Object -Last 1 + if ($result -eq 'null') { + Write-Warning "Could not find DcpModelCreation events in the trace" + return $null + } + + $duration = [double]::Parse($result, [System.Globalization.CultureInfo]::InvariantCulture) + Write-Verbose "Calculated duration: $duration ms" + return $duration + } + catch { + Write-Warning "Error parsing trace: $_" + return $null + } +} + +# Main execution +function Main { + Write-Host "==================================================" -ForegroundColor Cyan + Write-Host " Aspire Startup Performance Measurement" -ForegroundColor Cyan + Write-Host "==================================================" -ForegroundColor Cyan + Write-Host "" + Write-Host "Project: $AppHostName" + Write-Host "Project Path: $AppHostProject" + Write-Host "Iterations: $Iterations" + Write-Host "Trace Duration: $TraceDurationSeconds seconds" + Write-Host "Pause Between Iterations: $PauseBetweenIterationsSeconds seconds" + Write-Host "Preserve Traces: $PreserveTraces" + if ($PreserveTraces -or $TraceOutputDirectory) { + Write-Host "Trace Directory: $OutputDirectory" + } + Write-Host "" + + Test-Prerequisites + + # Build the TraceAnalyzer tool for parsing traces + $traceAnalyzerAvailable = Build-TraceAnalyzer + + # Ensure output directory exists + if (-not (Test-Path $OutputDirectory)) { + New-Item -ItemType Directory -Path $OutputDirectory -Force | Out-Null + } + + if (-not $SkipBuild) { + Build-AppHost + } + else { + Write-Host "Skipping build (SkipBuild flag set)" -ForegroundColor Yellow + } + + $results = @() + $timestamp = Get-Date -Format 'yyyyMMdd_HHmmss' + + try { + for ($i = 1; $i -le $Iterations; $i++) { + $traceBaseName = "${AppHostName}_startup_${timestamp}_iter${i}" + $traceOutputPath = Join-Path $OutputDirectory $traceBaseName + + $tracePath = Invoke-PerformanceIteration -IterationNumber $i -TraceOutputPath $traceOutputPath + + if ($tracePath -and (Test-Path $tracePath)) { + $duration = $null + if ($traceAnalyzerAvailable) { + $duration = Get-StartupTiming -TracePath $tracePath + } + + if ($null -ne $duration) { + $results += [PSCustomObject]@{ + Iteration = $i + TracePath = $tracePath + StartupTimeMs = [math]::Round($duration, 2) + } + Write-Host "Startup time: $([math]::Round($duration, 2)) ms" -ForegroundColor Green + } + else { + $results += [PSCustomObject]@{ + Iteration = $i + TracePath = $tracePath + StartupTimeMs = $null + } + Write-Host "Trace collected: $tracePath" -ForegroundColor Green + } + } + else { + Write-Warning "No trace file generated for iteration $i" + } + + # Pause between iterations + if ($i -lt $Iterations -and $PauseBetweenIterationsSeconds -gt 0) { + Write-Verbose "Pausing for $PauseBetweenIterationsSeconds seconds before next iteration..." + Start-Sleep -Seconds $PauseBetweenIterationsSeconds + } + } + } + finally { + # Clean up temporary trace directory if not preserving traces + if ($ShouldCleanupDirectory -and (Test-Path $OutputDirectory)) { + Write-Verbose "Cleaning up temporary trace directory: $OutputDirectory" + Remove-Item -Path $OutputDirectory -Recurse -Force -ErrorAction SilentlyContinue + } + } + + # Summary + Write-Host "" + Write-Host "==================================================" -ForegroundColor Cyan + Write-Host " Results Summary" -ForegroundColor Cyan + Write-Host "==================================================" -ForegroundColor Cyan + + # Wrap in @() to ensure array even with single/null results + $validResults = @($results | Where-Object { $null -ne $_.StartupTimeMs }) + + if ($validResults.Count -gt 0) { + Write-Host "" + # Only show TracePath in summary if PreserveTraces is set + if ($PreserveTraces) { + $results | Format-Table -AutoSize + } + else { + $results | Select-Object Iteration, StartupTimeMs | Format-Table -AutoSize + } + + $times = @($validResults | ForEach-Object { $_.StartupTimeMs }) + $avg = ($times | Measure-Object -Average).Average + $min = ($times | Measure-Object -Minimum).Minimum + $max = ($times | Measure-Object -Maximum).Maximum + + Write-Host "" + Write-Host "Statistics:" -ForegroundColor Yellow + Write-Host " Successful iterations: $($validResults.Count) / $Iterations" + Write-Host " Minimum: $([math]::Round($min, 2)) ms" + Write-Host " Maximum: $([math]::Round($max, 2)) ms" + Write-Host " Average: $([math]::Round($avg, 2)) ms" + + if ($validResults.Count -gt 1) { + $stdDev = [math]::Sqrt(($times | ForEach-Object { [math]::Pow($_ - $avg, 2) } | Measure-Object -Average).Average) + Write-Host " Std Dev: $([math]::Round($stdDev, 2)) ms" + } + + if ($PreserveTraces) { + Write-Host "" + Write-Host "Trace files saved to: $OutputDirectory" -ForegroundColor Cyan + } + } + elseif ($results.Count -gt 0) { + Write-Host "" + Write-Host "Collected $($results.Count) trace(s) but could not extract timing." -ForegroundColor Yellow + if ($PreserveTraces) { + Write-Host "" + Write-Host "Trace files saved to: $OutputDirectory" -ForegroundColor Cyan + $results | Select-Object Iteration, TracePath | Format-Table -AutoSize + Write-Host "" + Write-Host "Open traces in PerfView or Visual Studio to analyze startup timing." -ForegroundColor Yellow + } + } + else { + Write-Warning "No traces were collected." + } + + return $results +} + +# Run the script +Main diff --git a/tools/perf/TraceAnalyzer/Program.cs b/tools/perf/TraceAnalyzer/Program.cs new file mode 100644 index 00000000000..76ffe45d44d --- /dev/null +++ b/tools/perf/TraceAnalyzer/Program.cs @@ -0,0 +1,80 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Tool to analyze .nettrace files and extract Aspire startup timing information. +// Usage: dotnet run -- +// Output: Prints the startup duration in milliseconds to stdout, or "null" if events not found. + +using Microsoft.Diagnostics.Tracing; + +if (args.Length == 0) +{ + Console.Error.WriteLine("Usage: TraceAnalyzer "); + return 1; +} + +var tracePath = args[0]; + +if (!File.Exists(tracePath)) +{ + Console.Error.WriteLine($"Error: File not found: {tracePath}"); + return 1; +} + +// Event IDs from AspireEventSource +const int DcpModelCreationStartEventId = 17; +const int DcpModelCreationStopEventId = 18; + +const string AspireHostingProviderName = "Microsoft-Aspire-Hosting"; + +try +{ + double? startTime = null; + double? stopTime = null; + + using (var source = new EventPipeEventSource(tracePath)) + { + source.Dynamic.AddCallbackForProviderEvents((string pName, string eName) => + { + if (pName != AspireHostingProviderName) + { + return EventFilterResponse.RejectProvider; + } + if (eName == null || eName.StartsWith("DcpModelCreation", StringComparison.Ordinal)) + { + return EventFilterResponse.AcceptEvent; + } + return EventFilterResponse.RejectEvent; + }, + (TraceEvent traceEvent) => + { + if ((int)traceEvent.ID == DcpModelCreationStartEventId) + { + startTime = traceEvent.TimeStampRelativeMSec; + } + else if ((int)traceEvent.ID == DcpModelCreationStopEventId) + { + stopTime = traceEvent.TimeStampRelativeMSec; + } + }); + + source.Process(); + } + + if (startTime.HasValue && stopTime.HasValue) + { + var duration = stopTime.Value - startTime.Value; + Console.WriteLine(duration.ToString("F2", System.Globalization.CultureInfo.InvariantCulture)); + return 0; + } + else + { + Console.WriteLine("null"); + return 0; + } +} +catch (Exception ex) +{ + Console.Error.WriteLine($"Error parsing trace: {ex.Message}"); + return 1; +} diff --git a/tools/perf/TraceAnalyzer/TraceAnalyzer.csproj b/tools/perf/TraceAnalyzer/TraceAnalyzer.csproj new file mode 100644 index 00000000000..f984521fbc3 --- /dev/null +++ b/tools/perf/TraceAnalyzer/TraceAnalyzer.csproj @@ -0,0 +1,16 @@ + + + + Exe + net8.0 + enable + enable + + false + + + + + + + From 77b2215ed4411ca7da277a240958d88d3476eed8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 21:17:04 +0000 Subject: [PATCH 2/5] Initial plan From 4b027657be3e5e66828f2d6288b8010ea385dd34 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 21:26:51 +0000 Subject: [PATCH 3/5] Add AzureNetworkServiceTags class with common Azure service tags and tests Co-authored-by: eerhardt <8291187+eerhardt@users.noreply.github.com> --- .../AzureNetworkServiceTags.cs | 111 ++++++++++++++++++ .../AzureVirtualNetworkExtensionsTests.cs | 60 ++++++++++ 2 files changed, 171 insertions(+) create mode 100644 src/Aspire.Hosting.Azure.Network/AzureNetworkServiceTags.cs diff --git a/src/Aspire.Hosting.Azure.Network/AzureNetworkServiceTags.cs b/src/Aspire.Hosting.Azure.Network/AzureNetworkServiceTags.cs new file mode 100644 index 00000000000..529562afc82 --- /dev/null +++ b/src/Aspire.Hosting.Azure.Network/AzureNetworkServiceTags.cs @@ -0,0 +1,111 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Hosting.Azure; + +/// +/// Provides well-known Azure service tags that can be used as source or destination address prefixes +/// in network security group rules. +/// +/// +/// +/// Service tags represent a group of IP address prefixes from a given Azure service. Microsoft manages the +/// address prefixes encompassed by each tag and automatically updates them as addresses change. +/// +/// +/// These tags can be used with the from and to parameters of methods such as +/// , , +/// , , +/// or with the and properties. +/// +/// +/// +/// Use service tags when configuring network security rules: +/// +/// var subnet = vnet.AddSubnet("web", "10.0.1.0/24") +/// .AllowInbound(port: "443", from: AzureNetworkServiceTags.AzureLoadBalancer, protocol: SecurityRuleProtocol.Tcp) +/// .DenyInbound(from: AzureNetworkServiceTags.Internet); +/// +/// +public static class AzureNetworkServiceTags +{ + /// + /// Represents the Internet address space, including all publicly routable IP addresses. + /// + public const string Internet = "Internet"; + + /// + /// Represents the address space for the virtual network, including all connected address spaces, + /// all connected on-premises address spaces, and peered virtual networks. + /// + public const string VirtualNetwork = "VirtualNetwork"; + + /// + /// Represents the Azure infrastructure load balancer. This tag is commonly used to allow + /// health probe traffic from Azure. + /// + public const string AzureLoadBalancer = "AzureLoadBalancer"; + + /// + /// Represents Azure Traffic Manager probe IP addresses. + /// + public const string AzureTrafficManager = "AzureTrafficManager"; + + /// + /// Represents the Azure Storage service. This tag does not include specific Storage accounts; + /// it covers all Azure Storage IP addresses. + /// + public const string Storage = "Storage"; + + /// + /// Represents Azure SQL Database, Azure Database for MySQL, Azure Database for PostgreSQL, + /// Azure Database for MariaDB, and Azure Synapse Analytics. + /// + public const string Sql = "Sql"; + + /// + /// Represents Azure Cosmos DB service addresses. + /// + public const string AzureCosmosDB = "AzureCosmosDB"; + + /// + /// Represents Azure Key Vault service addresses. + /// + public const string AzureKeyVault = "AzureKeyVault"; + + /// + /// Represents Azure Event Hubs service addresses. + /// + public const string EventHub = "EventHub"; + + /// + /// Represents Azure Service Bus service addresses. + /// + public const string ServiceBus = "ServiceBus"; + + /// + /// Represents Azure Container Registry service addresses. + /// + public const string AzureContainerRegistry = "AzureContainerRegistry"; + + /// + /// Represents Azure App Service and Azure Functions service addresses. + /// + public const string AppService = "AppService"; + + /// + /// Represents Microsoft Entra ID (formerly Azure Active Directory) service addresses. + /// + public const string AzureActiveDirectory = "AzureActiveDirectory"; + + /// + /// Represents Azure Monitor service addresses, including Log Analytics, Application Insights, + /// and Azure Monitor metrics. + /// + public const string AzureMonitor = "AzureMonitor"; + + /// + /// Represents the Gateway Manager service, used for VPN Gateway and Application Gateway management traffic. + /// + public const string GatewayManager = "GatewayManager"; +} diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureVirtualNetworkExtensionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureVirtualNetworkExtensionsTests.cs index 08fafbde1a3..4cc1b461084 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureVirtualNetworkExtensionsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureVirtualNetworkExtensionsTests.cs @@ -361,6 +361,66 @@ await Verify(vnetManifest.BicepText, extension: "bicep") .AppendContentAsFile(nsgManifest.BicepText, "bicep", "nsg"); } + [Fact] + public void ServiceTags_CanBeUsedAsFromAndToParameters() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + var vnet = builder.AddAzureVirtualNetwork("myvnet"); + var subnet = vnet.AddSubnet("web", "10.0.1.0/24") + .AllowInbound(port: "443", from: AzureNetworkServiceTags.AzureLoadBalancer, protocol: SecurityRuleProtocol.Tcp) + .DenyInbound(from: AzureNetworkServiceTags.Internet) + .AllowOutbound(port: "443", to: AzureNetworkServiceTags.Storage) + .DenyOutbound(to: AzureNetworkServiceTags.VirtualNetwork); + + var rules = subnet.Resource.NetworkSecurityGroup!.SecurityRules; + Assert.Equal(4, rules.Count); + + Assert.Equal("AzureLoadBalancer", rules[0].SourceAddressPrefix); + Assert.Equal("Internet", rules[1].SourceAddressPrefix); + Assert.Equal("Storage", rules[2].DestinationAddressPrefix); + Assert.Equal("VirtualNetwork", rules[3].DestinationAddressPrefix); + } + + [Fact] + public void ServiceTags_CanBeUsedInSecurityRuleProperties() + { + var rule = new AzureSecurityRule + { + Name = "allow-https-from-lb", + Priority = 100, + Direction = SecurityRuleDirection.Inbound, + Access = SecurityRuleAccess.Allow, + Protocol = SecurityRuleProtocol.Tcp, + SourceAddressPrefix = AzureNetworkServiceTags.AzureLoadBalancer, + DestinationAddressPrefix = AzureNetworkServiceTags.VirtualNetwork, + DestinationPortRange = "443" + }; + + Assert.Equal("AzureLoadBalancer", rule.SourceAddressPrefix); + Assert.Equal("VirtualNetwork", rule.DestinationAddressPrefix); + } + + [Fact] + public void ServiceTags_HaveExpectedValues() + { + Assert.Equal("Internet", AzureNetworkServiceTags.Internet); + Assert.Equal("VirtualNetwork", AzureNetworkServiceTags.VirtualNetwork); + Assert.Equal("AzureLoadBalancer", AzureNetworkServiceTags.AzureLoadBalancer); + Assert.Equal("AzureTrafficManager", AzureNetworkServiceTags.AzureTrafficManager); + Assert.Equal("Storage", AzureNetworkServiceTags.Storage); + Assert.Equal("Sql", AzureNetworkServiceTags.Sql); + Assert.Equal("AzureCosmosDB", AzureNetworkServiceTags.AzureCosmosDB); + Assert.Equal("AzureKeyVault", AzureNetworkServiceTags.AzureKeyVault); + Assert.Equal("EventHub", AzureNetworkServiceTags.EventHub); + Assert.Equal("ServiceBus", AzureNetworkServiceTags.ServiceBus); + Assert.Equal("AzureContainerRegistry", AzureNetworkServiceTags.AzureContainerRegistry); + Assert.Equal("AppService", AzureNetworkServiceTags.AppService); + Assert.Equal("AzureActiveDirectory", AzureNetworkServiceTags.AzureActiveDirectory); + Assert.Equal("AzureMonitor", AzureNetworkServiceTags.AzureMonitor); + Assert.Equal("GatewayManager", AzureNetworkServiceTags.GatewayManager); + } + [Fact] public void AllFourDirectionAccessCombos_SetCorrectly() { From dba228639d6509da4f6ac09868f4d360bd41c93e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 21:57:58 +0000 Subject: [PATCH 4/5] Use nameof pattern for AzureNetworkServiceTags constants Co-authored-by: eerhardt <8291187+eerhardt@users.noreply.github.com> --- .../AzureNetworkServiceTags.cs | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Aspire.Hosting.Azure.Network/AzureNetworkServiceTags.cs b/src/Aspire.Hosting.Azure.Network/AzureNetworkServiceTags.cs index 529562afc82..8eaeb2be4db 100644 --- a/src/Aspire.Hosting.Azure.Network/AzureNetworkServiceTags.cs +++ b/src/Aspire.Hosting.Azure.Network/AzureNetworkServiceTags.cs @@ -32,80 +32,80 @@ public static class AzureNetworkServiceTags /// /// Represents the Internet address space, including all publicly routable IP addresses. /// - public const string Internet = "Internet"; + public const string Internet = nameof(Internet); /// /// Represents the address space for the virtual network, including all connected address spaces, /// all connected on-premises address spaces, and peered virtual networks. /// - public const string VirtualNetwork = "VirtualNetwork"; + public const string VirtualNetwork = nameof(VirtualNetwork); /// /// Represents the Azure infrastructure load balancer. This tag is commonly used to allow /// health probe traffic from Azure. /// - public const string AzureLoadBalancer = "AzureLoadBalancer"; + public const string AzureLoadBalancer = nameof(AzureLoadBalancer); /// /// Represents Azure Traffic Manager probe IP addresses. /// - public const string AzureTrafficManager = "AzureTrafficManager"; + public const string AzureTrafficManager = nameof(AzureTrafficManager); /// /// Represents the Azure Storage service. This tag does not include specific Storage accounts; /// it covers all Azure Storage IP addresses. /// - public const string Storage = "Storage"; + public const string Storage = nameof(Storage); /// /// Represents Azure SQL Database, Azure Database for MySQL, Azure Database for PostgreSQL, /// Azure Database for MariaDB, and Azure Synapse Analytics. /// - public const string Sql = "Sql"; + public const string Sql = nameof(Sql); /// /// Represents Azure Cosmos DB service addresses. /// - public const string AzureCosmosDB = "AzureCosmosDB"; + public const string AzureCosmosDB = nameof(AzureCosmosDB); /// /// Represents Azure Key Vault service addresses. /// - public const string AzureKeyVault = "AzureKeyVault"; + public const string AzureKeyVault = nameof(AzureKeyVault); /// /// Represents Azure Event Hubs service addresses. /// - public const string EventHub = "EventHub"; + public const string EventHub = nameof(EventHub); /// /// Represents Azure Service Bus service addresses. /// - public const string ServiceBus = "ServiceBus"; + public const string ServiceBus = nameof(ServiceBus); /// /// Represents Azure Container Registry service addresses. /// - public const string AzureContainerRegistry = "AzureContainerRegistry"; + public const string AzureContainerRegistry = nameof(AzureContainerRegistry); /// /// Represents Azure App Service and Azure Functions service addresses. /// - public const string AppService = "AppService"; + public const string AppService = nameof(AppService); /// /// Represents Microsoft Entra ID (formerly Azure Active Directory) service addresses. /// - public const string AzureActiveDirectory = "AzureActiveDirectory"; + public const string AzureActiveDirectory = nameof(AzureActiveDirectory); /// /// Represents Azure Monitor service addresses, including Log Analytics, Application Insights, /// and Azure Monitor metrics. /// - public const string AzureMonitor = "AzureMonitor"; + public const string AzureMonitor = nameof(AzureMonitor); /// /// Represents the Gateway Manager service, used for VPN Gateway and Application Gateway management traffic. /// - public const string GatewayManager = "GatewayManager"; + public const string GatewayManager = nameof(GatewayManager); } From 3a6d50cd5e57b5e54d1e32eeebb0fac7b5c01eda Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:51:28 +0000 Subject: [PATCH 5/5] Rename AzureNetworkServiceTags to AzureServiceTags Co-authored-by: eerhardt <8291187+eerhardt@users.noreply.github.com> --- ...workServiceTags.cs => AzureServiceTags.cs} | 6 +-- .../AzureVirtualNetworkExtensionsTests.cs | 42 +++++++++---------- 2 files changed, 24 insertions(+), 24 deletions(-) rename src/Aspire.Hosting.Azure.Network/{AzureNetworkServiceTags.cs => AzureServiceTags.cs} (94%) diff --git a/src/Aspire.Hosting.Azure.Network/AzureNetworkServiceTags.cs b/src/Aspire.Hosting.Azure.Network/AzureServiceTags.cs similarity index 94% rename from src/Aspire.Hosting.Azure.Network/AzureNetworkServiceTags.cs rename to src/Aspire.Hosting.Azure.Network/AzureServiceTags.cs index 8eaeb2be4db..d09d8b61ec6 100644 --- a/src/Aspire.Hosting.Azure.Network/AzureNetworkServiceTags.cs +++ b/src/Aspire.Hosting.Azure.Network/AzureServiceTags.cs @@ -23,11 +23,11 @@ namespace Aspire.Hosting.Azure; /// Use service tags when configuring network security rules: /// /// var subnet = vnet.AddSubnet("web", "10.0.1.0/24") -/// .AllowInbound(port: "443", from: AzureNetworkServiceTags.AzureLoadBalancer, protocol: SecurityRuleProtocol.Tcp) -/// .DenyInbound(from: AzureNetworkServiceTags.Internet); +/// .AllowInbound(port: "443", from: AzureServiceTags.AzureLoadBalancer, protocol: SecurityRuleProtocol.Tcp) +/// .DenyInbound(from: AzureServiceTags.Internet); /// /// -public static class AzureNetworkServiceTags +public static class AzureServiceTags { /// /// Represents the Internet address space, including all publicly routable IP addresses. diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureVirtualNetworkExtensionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureVirtualNetworkExtensionsTests.cs index 4cc1b461084..1747a3b1f96 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureVirtualNetworkExtensionsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureVirtualNetworkExtensionsTests.cs @@ -368,10 +368,10 @@ public void ServiceTags_CanBeUsedAsFromAndToParameters() var vnet = builder.AddAzureVirtualNetwork("myvnet"); var subnet = vnet.AddSubnet("web", "10.0.1.0/24") - .AllowInbound(port: "443", from: AzureNetworkServiceTags.AzureLoadBalancer, protocol: SecurityRuleProtocol.Tcp) - .DenyInbound(from: AzureNetworkServiceTags.Internet) - .AllowOutbound(port: "443", to: AzureNetworkServiceTags.Storage) - .DenyOutbound(to: AzureNetworkServiceTags.VirtualNetwork); + .AllowInbound(port: "443", from: AzureServiceTags.AzureLoadBalancer, protocol: SecurityRuleProtocol.Tcp) + .DenyInbound(from: AzureServiceTags.Internet) + .AllowOutbound(port: "443", to: AzureServiceTags.Storage) + .DenyOutbound(to: AzureServiceTags.VirtualNetwork); var rules = subnet.Resource.NetworkSecurityGroup!.SecurityRules; Assert.Equal(4, rules.Count); @@ -392,8 +392,8 @@ public void ServiceTags_CanBeUsedInSecurityRuleProperties() Direction = SecurityRuleDirection.Inbound, Access = SecurityRuleAccess.Allow, Protocol = SecurityRuleProtocol.Tcp, - SourceAddressPrefix = AzureNetworkServiceTags.AzureLoadBalancer, - DestinationAddressPrefix = AzureNetworkServiceTags.VirtualNetwork, + SourceAddressPrefix = AzureServiceTags.AzureLoadBalancer, + DestinationAddressPrefix = AzureServiceTags.VirtualNetwork, DestinationPortRange = "443" }; @@ -404,21 +404,21 @@ public void ServiceTags_CanBeUsedInSecurityRuleProperties() [Fact] public void ServiceTags_HaveExpectedValues() { - Assert.Equal("Internet", AzureNetworkServiceTags.Internet); - Assert.Equal("VirtualNetwork", AzureNetworkServiceTags.VirtualNetwork); - Assert.Equal("AzureLoadBalancer", AzureNetworkServiceTags.AzureLoadBalancer); - Assert.Equal("AzureTrafficManager", AzureNetworkServiceTags.AzureTrafficManager); - Assert.Equal("Storage", AzureNetworkServiceTags.Storage); - Assert.Equal("Sql", AzureNetworkServiceTags.Sql); - Assert.Equal("AzureCosmosDB", AzureNetworkServiceTags.AzureCosmosDB); - Assert.Equal("AzureKeyVault", AzureNetworkServiceTags.AzureKeyVault); - Assert.Equal("EventHub", AzureNetworkServiceTags.EventHub); - Assert.Equal("ServiceBus", AzureNetworkServiceTags.ServiceBus); - Assert.Equal("AzureContainerRegistry", AzureNetworkServiceTags.AzureContainerRegistry); - Assert.Equal("AppService", AzureNetworkServiceTags.AppService); - Assert.Equal("AzureActiveDirectory", AzureNetworkServiceTags.AzureActiveDirectory); - Assert.Equal("AzureMonitor", AzureNetworkServiceTags.AzureMonitor); - Assert.Equal("GatewayManager", AzureNetworkServiceTags.GatewayManager); + Assert.Equal("Internet", AzureServiceTags.Internet); + Assert.Equal("VirtualNetwork", AzureServiceTags.VirtualNetwork); + Assert.Equal("AzureLoadBalancer", AzureServiceTags.AzureLoadBalancer); + Assert.Equal("AzureTrafficManager", AzureServiceTags.AzureTrafficManager); + Assert.Equal("Storage", AzureServiceTags.Storage); + Assert.Equal("Sql", AzureServiceTags.Sql); + Assert.Equal("AzureCosmosDB", AzureServiceTags.AzureCosmosDB); + Assert.Equal("AzureKeyVault", AzureServiceTags.AzureKeyVault); + Assert.Equal("EventHub", AzureServiceTags.EventHub); + Assert.Equal("ServiceBus", AzureServiceTags.ServiceBus); + Assert.Equal("AzureContainerRegistry", AzureServiceTags.AzureContainerRegistry); + Assert.Equal("AppService", AzureServiceTags.AppService); + Assert.Equal("AzureActiveDirectory", AzureServiceTags.AzureActiveDirectory); + Assert.Equal("AzureMonitor", AzureServiceTags.AzureMonitor); + Assert.Equal("GatewayManager", AzureServiceTags.GatewayManager); } [Fact]