Skip to content

Add TryGetByName to IResourceCollection#16389

Merged
JamesNK merged 9 commits intomainfrom
copilot/add-trygetbyname-method
Apr 23, 2026
Merged

Add TryGetByName to IResourceCollection#16389
JamesNK merged 9 commits intomainfrom
copilot/add-trygetbyname-method

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 22, 2026

Description

Resource lookup by name was verbose, duplicated across the codebase, and O(n):

// Before — repeated everywhere with slight variations
var resource = model.Resources.SingleOrDefault(r => StringComparers.ResourceName.Equals(r.Name, name));

Adds TryGetByName as a default interface method on IResourceCollection with a dictionary-backed O(1) override in ResourceCollection:

// After
if (model.Resources.TryGetByName(name, out var resource))
{
    // ...
}

Changes:

  • IResourceCollection — added TryGetByName(string, out IResource?) as a DIM with enumerator-based fallback using StringComparisons.ResourceName, including <remarks> documentation
  • ResourceCollection — added Dictionary<string, IResource> (keyed with StringComparers.ResourceName) kept in sync across all mutation methods (Add, Remove, RemoveAt, Clear, Insert, indexer set); overrides TryGetByName for O(1) lookup with null-safe handling; enforces unique resource names across all mutation paths
  • Duplicate name error messagesThrowDuplicateResource includes both the new and existing resource types in the error message (e.g. "Cannot add resource of type 'X' with name '...' because resource of type 'Y' with that name already exists.")
  • Source files updated — replaced SingleOrDefault/FirstOrDefault + lambda name comparisons with TryGetByName calls across OtlpConfigurationExtensions, ContainerCreator, DistributedApplicationHostingTestingExtensions, PostgresBuilderExtensions, DistributedApplicationBuilder
  • Unit tests covering lookup, case insensitivity, not-found, empty collection, duplicate rejection (including existing type in error message), and dictionary sync for all mutation paths

Checklist

  • Is this feature complete?
    • Yes. Ready to ship.
    • No. Follow-up changes expected.
  • Are you including unit tests for the changes and scenario tests if relevant?
    • Yes
    • No
  • Did you add public API?
    • Yes
      • If yes, did you have an API Review for it?
        • Yes
        • No
      • Did you add <remarks /> and <code /> elements on your triple slash comments?
        • Yes
        • No
    • No
  • Does the change make any security assumptions or guarantees?
    • Yes
      • If yes, have you done a threat model and had a security review?
        • Yes
        • No
    • No
  • Does the change require an update in our Aspire docs?

Copilot AI linked an issue Apr 22, 2026 that may be closed by this pull request
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 22, 2026

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 16389

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 16389"

Copilot AI changed the title [WIP] Add TryGetByName method to IResourceCollection Add TryGetByName to IResourceCollection Apr 23, 2026
Copilot AI requested a review from JamesNK April 23, 2026 00:02
Comment thread src/Aspire.Hosting/ApplicationModel/IResourceCollection.cs Outdated
Comment thread src/Aspire.Hosting/ApplicationModel/ResourceCollection.cs Outdated
@JamesNK
Copy link
Copy Markdown
Member

JamesNK commented Apr 23, 2026

@davidfowl @eerhardt

What are your thoughts on TryGetByName vs GetByName. IResource is a reference type so returning null is fine.

The other thing to think about is whether a type check should be included, e.g. GetByName<IResourceWithEndpoints>("xxx"). I don't know off the top of my head with GetByName and GetByName<TResource> can exist as overloads together. I lean towards not including it. It's simple enough to do this:

var dashboardResource = model.Resources.GetByName("aspire-dashboard") as IResourceWithEndpoints;

Any strong opinions on using a DIM? If we don't like it then we could have an extension method for IResourceCollection. Internally it could try to case to ResourceCollection and then use an internal method on it. Otherwise it uses IResourceCollection + GetSingleOrDefault.

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 23, 2026

Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • 7k6vsblobprodcus337.vsblob.vsassets.io
    • Triggering command: /usr/share/dotnet/dotnet dotnet build src/Aspire.Hosting/Aspire.Hosting.csproj /p:SkipNativeBuild=true (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI requested a review from JamesNK April 23, 2026 00:08
@JamesNK JamesNK marked this pull request as ready for review April 23, 2026 00:10
Copilot AI review requested due to automatic review settings April 23, 2026 00:10
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a standardized, case-insensitive resource lookup API (TryGetByName) on IResourceCollection to replace repeated linear scans across the hosting codebase, with an internal ResourceCollection implementation optimized via a dictionary for O(1) lookups.

Changes:

  • Added IResourceCollection.TryGetByName(string, out IResource?) as a default interface method (enumerator-based fallback).
  • Implemented an O(1) override in ResourceCollection using a name-indexed dictionary kept in sync with mutations.
  • Updated multiple call sites to use TryGetByName instead of FirstOrDefault/SingleOrDefault + name comparisons; added unit tests.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/Aspire.Hosting.Tests/ApplicationModel/ResourceCollectionTests.cs Adds unit tests for TryGetByName behavior and dictionary sync across mutations.
src/Aspire.Hosting/ApplicationModel/IResourceCollection.cs Adds the public TryGetByName API as a default interface method fallback.
src/Aspire.Hosting/ApplicationModel/ResourceCollection.cs Adds dictionary-backed lookup and updates mutation paths to keep the index in sync.
src/Aspire.Hosting/Exec/ExecResourceManager.cs Switches exec target lookup to TryGetByName.
src/Aspire.Hosting/DistributedApplicationBuilderExtensions.cs Uses TryGetByName for builder resource lookup helpers.
src/Aspire.Hosting/DistributedApplicationBuilder.cs Uses TryGetByName to detect duplicate resource names during adds.
src/Aspire.Hosting/Dcp/OtlpEndpointReferenceGatherer.cs Uses TryGetByName to locate the dashboard resource.
src/Aspire.Hosting/Dashboard/DashboardEventHandlers.cs Uses TryGetByName for dashboard resource lookup.
src/Aspire.Hosting/BuiltInDistributedApplicationEventSubscriptionHandlers.cs Uses TryGetByName and removes an unused using.
src/Aspire.Hosting/Backchannel/DashboardUrlsHelper.cs Uses TryGetByName when resolving dashboard endpoints.
src/Aspire.Hosting/Backchannel/AuxiliaryBackchannelRpcTarget.cs Uses TryGetByName for resource resolution fallback.
src/Aspire.Hosting/Ats/ModelExports.cs Updates ATS export lookup helper to use TryGetByName.
src/Aspire.Hosting.PostgreSQL/PostgresBuilderExtensions.cs Uses TryGetByName to locate PostgresDatabaseResource instances by name.
Comments suppressed due to low confidence (1)

src/Aspire.Hosting/Exec/ExecResourceManager.cs:169

  • _execOptions.ResourceName! can still be null at runtime (e.g., config binding missing Exec:ResourceName), and TryGetByName/Dictionary.TryGetValue will throw ArgumentNullException. This changes behavior from the previous Equals check (which safely returned false for null) and can result in a less actionable failure. Consider validating ResourceName (null/empty) before lookup and returning the existing “not found” error path with a clear message.
            var targetResource = _model.Resources.TryGetByName(_execOptions.ResourceName!, out var resource) ? resource : null;
            if (targetResource is null)
            {
                _logger.LogError("Target resource '{ResourceName}' not found in the model resources.", _execOptions.ResourceName);
                throw new InvalidOperationException($"Target resource {_execOptions.ResourceName} not found in the model resources");

Comment thread src/Aspire.Hosting/ApplicationModel/ResourceCollection.cs
Comment thread src/Aspire.Hosting/ApplicationModel/IResourceCollection.cs
Comment thread src/Aspire.Hosting/ApplicationModel/ResourceCollection.cs
@mitchdenny
Copy link
Copy Markdown
Member

@davidfowl @eerhardt

What are your thoughts on TryGetByName vs GetByName. IResource is a reference type so returning null is fine.

I like having TryGetByName ... I think it leads to better coding patterns. If we have GetByName it show throw if that name is not found.

@mitchdenny
Copy link
Copy Markdown
Member

Not for this commit but this work probably lines us up to do something like ResourceAdded/ResourceRemoved events.

…esourceCollection

- Add TryGetByName as a DIM on IResourceCollection with dictionary-backed O(1) override in ResourceCollection
- Enforce unique resource names on Add, Insert, indexer set, and constructor in ResourceCollection
- Replace LINQ-based resource name lookups with TryGetByName across the codebase
- Remove redundant duplicate check from AddResource (now handled by ResourceCollection)
- Keep Build() duplicate check as safety net for non-ResourceCollection implementations
@JamesNK JamesNK force-pushed the copilot/add-trygetbyname-method branch from 816e65c to 8edc87d Compare April 23, 2026 01:16
@JamesNK
Copy link
Copy Markdown
Member

JamesNK commented Apr 23, 2026

@copilot fix build

Copy link
Copy Markdown
Member

@mitchdenny mitchdenny left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found (1 critical build failure, 1 minor diagnostic regression).

Comment thread src/Aspire.Hosting/ApplicationModel/ResourceCollection.cs
[DoesNotReturn]
private static void ThrowDuplicateResource(IResource newResource)
{
throw new DistributedApplicationException($"Cannot add resource of type '{newResource.GetType()}' with name '{newResource.Name}' because a resource with that name already exists. Resource names are case-insensitive.");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor diagnostic regression: The old duplicate check in AddResource included both resource types in the error message:

Cannot add resource of type '{new}' with name '...' because resource of type '{existing.GetType()}' with that name already exists.

This new ThrowDuplicateResource helper only mentions the new resource's type, losing the useful info about which existing resource conflicts. Consider accepting an optional IResource? existing parameter so callers that have it (like AddResource) can produce the richer message.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot fix

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 19d6b79. ThrowDuplicateResource now accepts an optional IResource? existingResource parameter, and all call sites pass _resourcesByName[item.Name] so the error message includes the existing resource's type (e.g. "resource of type 'TestResource'"), matching the original AddResource message.

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 23, 2026

@copilot fix build

Fixed in 28362a9. The TryGetByName DIM was missing from IResourceCollection — the previous commit added it only to the internal ResourceCollection class but not the public interface. Added the DIM with StringComparisons.ResourceName for the enumerator-based fallback, along with full XML docs including <remarks>.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • 7k6vsblobprodcus337.vsblob.vsassets.io
    • Triggering command: /usr/share/dotnet/dotnet dotnet build src/Aspire.Hosting/Aspire.Hosting.csproj --no-restore (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 23, 2026

Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • 7k6vsblobprodcus337.vsblob.vsassets.io
    • Triggering command: /usr/share/dotnet/dotnet dotnet build src/Aspire.Hosting/Aspire.Hosting.csproj --no-restore (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

@JamesNK JamesNK enabled auto-merge (squash) April 23, 2026 05:29
@JamesNK JamesNK requested a review from eerhardt as a code owner April 23, 2026 05:58
Comment on lines +172 to +175
if (appModel.Resources.TryGetByName("workload", out var existingResource) && existingResource is AksNodePoolResource existingPool)
{
return existingPool;
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mitchdenny Is this correct? Multiple workload resources were being added so this was needed to stop MultiEnv_ResourcesMatchCorrectEnvironment from failing.

Also, workload is a pretty generic name. Someone could have a resource call workload in their own app and they'll get weird behavior.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leave it as workload for now, I'll probably revisit with a different name for the resource vs. the Azure sub-resource name in AKS (which we'll want to align to the common default for AKS deployments)

Comment on lines +395 to +398
if (appModel.Resources.TryGetByName(roleAssignmentResource.Name, out _))
{
continue;
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eerhardt Is this correct? Multiple role assignment resources with the same name were being added so this was needed to stop AddFoundry_GeneratesValidBicep from failing.

@JamesNK JamesNK merged commit 31165fc into main Apr 23, 2026
557 of 565 checks passed
@github-actions github-actions Bot added this to the 13.3 milestone Apr 23, 2026
@github-actions
Copy link
Copy Markdown
Contributor

🎬 CLI E2E Test Recordings — 74 recordings uploaded (commit 653bb8b)

View recordings
Test Recording
AddPackageInteractiveWhileAppHostRunningDetached ▶️ View Recording
AddPackageWhileAppHostRunningDetached ▶️ View Recording
AgentCommands_AllHelpOutputs_AreCorrect ▶️ View Recording
AgentInitCommand_DefaultSelection_InstallsSkillOnly ▶️ View Recording
AgentInitCommand_MigratesDeprecatedConfig ▶️ View Recording
AspireAddPackageVersionToDirectoryPackagesProps ▶️ View Recording
AspireUpdateRemovesAppHostPackageVersionFromDirectoryPackagesProps ▶️ View Recording
Banner_DisplayedOnFirstRun ▶️ View Recording
Banner_DisplayedWithExplicitFlag ▶️ View Recording
Banner_NotDisplayedWithNoLogoFlag ▶️ View Recording
CertificatesClean_RemovesCertificates ▶️ View Recording
CertificatesTrust_WithNoCert_CreatesAndTrustsCertificate ▶️ View Recording
CertificatesTrust_WithUntrustedCert_TrustsCertificate ▶️ View Recording
ConfigSetGet_CreatesNestedJsonFormat ▶️ View Recording
CreateAndRunAspireStarterProject ▶️ View Recording
CreateAndRunAspireStarterProjectWithBundle ▶️ View Recording
CreateAndRunEmptyAppHostProject ▶️ View Recording
CreateAndRunJavaEmptyAppHostProject ▶️ View Recording
CreateAndRunJsReactProject ▶️ View Recording
CreateAndRunPythonReactProject ▶️ View Recording
CreateAndRunTypeScriptEmptyAppHostProject ▶️ View Recording
CreateAndRunTypeScriptStarterProject ▶️ View Recording
CreateJavaAppHostWithViteApp ▶️ View Recording
CreateTypeScriptAppHostWithViteApp_UsesConfiguredToolchain ▶️ View Recording
DashboardRunWithOtelTracesReturnsNoTraces ▶️ View Recording
DeployK8sBasicApiService ▶️ View Recording
DeployK8sWithGarnet ▶️ View Recording
DeployK8sWithMongoDB ▶️ View Recording
DeployK8sWithMySql ▶️ View Recording
DeployK8sWithPostgres ▶️ View Recording
DeployK8sWithRabbitMQ ▶️ View Recording
DeployK8sWithRedis ▶️ View Recording
DeployK8sWithSqlServer ▶️ View Recording
DeployK8sWithValkey ▶️ View Recording
DeployTypeScriptAppToKubernetes ▶️ View Recording
DescribeCommandResolvesReplicaNames ▶️ View Recording
DescribeCommandShowsRunningResources ▶️ View Recording
DetachFormatJsonProducesValidJson ▶️ View Recording
DetachFormatJsonProducesValidJsonWhenRestartingExistingInstance ▶️ View Recording
DoListStepsShowsPipelineSteps ▶️ View Recording
DoctorCommand_DetectsDeprecatedAgentConfig ▶️ View Recording
DoctorCommand_TypeScriptAppHostReportsMissingConfiguredToolchain ▶️ View Recording
DoctorCommand_WithSslCertDir_ShowsTrusted ▶️ View Recording
DoctorCommand_WithoutSslCertDir_ShowsPartiallyTrusted ▶️ View Recording
GlobalMigration_HandlesCommentsAndTrailingCommas ▶️ View Recording
GlobalMigration_HandlesMalformedLegacyJson ▶️ View Recording
GlobalMigration_PreservesAllValueTypes ▶️ View Recording
GlobalMigration_SkipsWhenNewConfigExists ▶️ View Recording
GlobalSettings_MigratedFromLegacyFormat ▶️ View Recording
InitTypeScriptAppHost_AugmentsExistingViteRepoAtRoot ▶️ View Recording
InvalidAppHostPathWithComments_IsHealedOnRun ▶️ View Recording
LegacySettingsMigration_AdjustsRelativeAppHostPath ▶️ View Recording
LogsCommandShowsResourceLogs ▶️ View Recording
OtelLogsReturnsStructuredLogsFromStarterAppCore ▶️ View Recording
PsCommandListsRunningAppHost ▶️ View Recording
PsFormatJsonOutputsOnlyJsonToStdout ▶️ View Recording
PublishWithConfigureEnvFileUpdatesEnvOutput ▶️ View Recording
PublishWithDockerComposeServiceCallbackSucceeds ▶️ View Recording
PublishWithoutOutputPathUsesAppHostDirectoryDefault ▶️ View Recording
RestoreGeneratesSdkFiles ▶️ View Recording
RestoreGeneratesSdkFiles_WithConfiguredToolchain ▶️ View Recording
RestoreRefreshesGeneratedSdkAfterAddingIntegration ▶️ View Recording
RestoreSupportsConfigOnlyHelperPackageAndCrossPackageTypes ▶️ View Recording
RunFromParentDirectory_UsesExistingConfigNearAppHost ▶️ View Recording
SecretCrudOnDotNetAppHost ▶️ View Recording
SecretCrudOnTypeScriptAppHost ▶️ View Recording
StagingChannel_ConfigureAndVerifySettings_ThenSwitchChannels ▶️ View Recording
StartAndWaitForTypeScriptSqlServerAppHostWithNativeAssets ▶️ View Recording
StopAllAppHostsFromAppHostDirectory ▶️ View Recording
StopAllAppHostsFromUnrelatedDirectory ▶️ View Recording
StopNonInteractiveMultipleAppHostsShowsError ▶️ View Recording
StopNonInteractiveSingleAppHost ▶️ View Recording
StopWithNoRunningAppHostExitsSuccessfully ▶️ View Recording
UnAwaitedChainsCompileWithAutoResolvePromises ▶️ View Recording

📹 Recordings uploaded automatically from CI run #24821426898

@aspire-repo-bot
Copy link
Copy Markdown
Contributor

No documentation PR is required for this change.

Reason: This PR adds TryGetByName as a convenience method on IResourceCollection — a low-level utility API intended for extension/AppHost authors. The aspire.dev documentation site focuses on user-facing features, integrations, and getting started content rather than individual utility methods on hosting interfaces. This change doesn't introduce new resources, integrations, CLI commands, or user-facing configuration patterns that would require prose documentation.

Generated by PR Documentation Check for issue #16389 · ● 208.1K ·

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add TryGetByName

5 participants