Skip to content

Commit dd46ca9

Browse files
authored
aspire: Consider Aspire Capability when detecting AppHost (#4447)
In addition to the `IsAspireHost` property we will now look at the project capabilities to see if an `Aspire` capability is listed and if treat the project as an AppHost project. This aligns `azd`'s behavior with other tooling like Visual Studio which uses the project capabilities to determine if the project is an App Host or not. The .NET Team asked us to include this in our sniffing logic (but to continue to check `IsAspireHost` as well). Fixes #4364
1 parent 209fd01 commit dd46ca9

File tree

5 files changed

+94
-22
lines changed

5 files changed

+94
-22
lines changed

cli/azd/internal/appdetect/dotnet_apphost.go

+1-13
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"io/fs"
66
"log"
77
"path/filepath"
8-
"strings"
98

109
"github.com/azure/azure-dev/cli/azd/pkg/tools/dotnet"
1110
)
@@ -26,7 +25,7 @@ func (ad *dotNetAppHostDetector) DetectProject(ctx context.Context, path string,
2625
switch ext {
2726
case ".csproj", ".fsproj", ".vbproj":
2827
projectPath := filepath.Join(path, name)
29-
if isAppHost, err := ad.isAppHostProject(ctx, filepath.Join(projectPath)); err != nil {
28+
if isAppHost, err := ad.dotnetCli.IsAspireHostProject(ctx, filepath.Join(projectPath)); err != nil {
3029
log.Printf("error checking if %s is an app host project: %v", projectPath, err)
3130
} else if isAppHost {
3231
return &Project{
@@ -40,14 +39,3 @@ func (ad *dotNetAppHostDetector) DetectProject(ctx context.Context, path string,
4039

4140
return nil, nil
4241
}
43-
44-
// isAppHostProject returns true if the project at the given path has an MS Build Property named "IsAspireHost" which is
45-
// set to "true".
46-
func (ad *dotNetAppHostDetector) isAppHostProject(ctx context.Context, projectPath string) (bool, error) {
47-
value, err := ad.dotnetCli.GetMsBuildProperty(ctx, projectPath, "IsAspireHost")
48-
if err != nil {
49-
return false, err
50-
}
51-
52-
return strings.TrimSpace(value) == "true", nil
53-
}

cli/azd/internal/vsrpc/utils.go

+2-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"fmt"
77
"log"
88
"path/filepath"
9-
"strings"
109

1110
"github.com/azure/azure-dev/cli/azd/pkg/apphost"
1211
"github.com/azure/azure-dev/cli/azd/pkg/environment/azdcontext"
@@ -20,11 +19,11 @@ func appHostForProject(
2019
) (*project.ServiceConfig, error) {
2120
for _, service := range pc.Services {
2221
if service.Language == project.ServiceLanguageDotNet {
23-
isAppHost, err := dotnetCli.GetMsBuildProperty(ctx, service.Path(), "IsAspireHost")
22+
isAppHost, err := dotnetCli.IsAspireHostProject(ctx, service.Path())
2423
if err != nil {
2524
log.Printf("error checking if %s is an app host project: %v", service.Path(), err)
2625
}
27-
if strings.TrimSpace(isAppHost) == "true" {
26+
if isAppHost {
2827
return service, nil
2928
}
3029
}

cli/azd/pkg/project/dotnet_importer.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ func (ai *DotNetImporter) CanImport(ctx context.Context, projectPath string) (bo
8383
return v.is, v.err
8484
}
8585

86-
value, err := ai.dotnetCli.GetMsBuildProperty(ctx, projectPath, "IsAspireHost")
86+
isAppHost, err := ai.dotnetCli.IsAspireHostProject(ctx, projectPath)
8787
if err != nil {
8888
ai.hostCheck[projectPath] = hostCheckResult{
8989
is: false,
@@ -94,11 +94,11 @@ func (ai *DotNetImporter) CanImport(ctx context.Context, projectPath string) (bo
9494
}
9595

9696
ai.hostCheck[projectPath] = hostCheckResult{
97-
is: strings.TrimSpace(value) == "true",
97+
is: isAppHost,
9898
err: nil,
9999
}
100100

101-
return strings.TrimSpace(value) == "true", nil
101+
return isAppHost, nil
102102
}
103103

104104
func (ai *DotNetImporter) ProjectInfrastructure(ctx context.Context, svcConfig *ServiceConfig) (*Infra, error) {

cli/azd/pkg/project/importer_test.go

+52-3
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ func TestImportManagerHasServiceErrorNoMultipleServicesWithAppHost(t *testing.T)
9292
slices.Contains(args.Args, "--getProperty:IsAspireHost")
9393
}).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) {
9494
return exec.RunResult{
95-
Stdout: "true",
95+
Stdout: aspireAppHostSniffResult,
9696
ExitCode: 0,
9797
}, nil
9898
})
@@ -145,7 +145,7 @@ func TestImportManagerHasServiceErrorAppHostMustTargetContainerApp(t *testing.T)
145145
slices.Contains(args.Args, "--getProperty:IsAspireHost")
146146
}).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) {
147147
return exec.RunResult{
148-
Stdout: "true",
148+
Stdout: aspireAppHostSniffResult,
149149
ExitCode: 0,
150150
}, nil
151151
})
@@ -278,7 +278,7 @@ func TestImportManagerProjectInfrastructureAspire(t *testing.T) {
278278
slices.Contains(args.Args, "--getProperty:IsAspireHost")
279279
}).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) {
280280
return exec.RunResult{
281-
Stdout: "true",
281+
Stdout: aspireAppHostSniffResult,
282282
ExitCode: 0,
283283
}, nil
284284
})
@@ -462,3 +462,52 @@ func TestImportManager_SynthAllInfrastructure_FromResources(t *testing.T) {
462462
_, err = im.SynthAllInfrastructure(context.Background(), prjConfig)
463463
assert.Error(t, err)
464464
}
465+
466+
// aspireAppHostSniffResult is mock data that would be returned by `dotnet msbuild` when fetching information about an
467+
// Aspire project. This is used to simulate the scenario where a project is an Aspire project. A real Aspire project would
468+
// have many entries in the ProjectCapability array (unrelated to the Aspire capability), but most have been omitted for
469+
// simplicity. An unrelated entry is included to ensure we are looking at the entire array of capabilities.
470+
// nolint: lll
471+
var aspireAppHostSniffResult string = `{
472+
"Properties": {
473+
"IsAspireHost": "true"
474+
},
475+
"Items": {
476+
"ProjectCapability": [
477+
{
478+
"Identity": "LocalUserSecrets",
479+
"FullPath": "/Users/matell/dd/ellismg/AspireBicep/AspireStarter/AspireStarter.AppHost/LocalUserSecrets",
480+
"RootDir": "/",
481+
"Filename": "LocalUserSecrets",
482+
"Extension": "",
483+
"RelativeDir": "",
484+
"Directory": "Users/matell/dd/ellismg/AspireBicep/AspireStarter/AspireStarter.AppHost/",
485+
"RecursiveDir": "",
486+
"ModifiedTime": "",
487+
"CreatedTime": "",
488+
"AccessedTime": "",
489+
"DefiningProjectFullPath": "/Users/matell/.nuget/packages/microsoft.extensions.configuration.usersecrets/8.0.0/buildTransitive/net6.0/Microsoft.Extensions.Configuration.UserSecrets.props",
490+
"DefiningProjectDirectory": "/Users/matell/.nuget/packages/microsoft.extensions.configuration.usersecrets/8.0.0/buildTransitive/net6.0/",
491+
"DefiningProjectName": "Microsoft.Extensions.Configuration.UserSecrets",
492+
"DefiningProjectExtension": ".props"
493+
},
494+
{
495+
"Identity": "Aspire",
496+
"FullPath": "/Users/matell/dd/ellismg/AspireBicep/AspireStarter/AspireStarter.AppHost/Aspire",
497+
"RootDir": "/",
498+
"Filename": "Aspire",
499+
"Extension": "",
500+
"RelativeDir": "",
501+
"Directory": "Users/matell/dd/ellismg/AspireBicep/AspireStarter/AspireStarter.AppHost/",
502+
"RecursiveDir": "",
503+
"ModifiedTime": "",
504+
"CreatedTime": "",
505+
"AccessedTime": "",
506+
"DefiningProjectFullPath": "/Users/matell/.nuget/packages/aspire.hosting.apphost/8.2.0/build/Aspire.Hosting.AppHost.targets",
507+
"DefiningProjectDirectory": "/Users/matell/.nuget/packages/aspire.hosting.apphost/8.2.0/build/",
508+
"DefiningProjectName": "Aspire.Hosting.AppHost",
509+
"DefiningProjectExtension": ".targets"
510+
}
511+
]
512+
}
513+
}`

cli/azd/pkg/tools/dotnet/dotnet.go

+36
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,42 @@ func (cli *Cli) GetMsBuildProperty(ctx context.Context, project string, property
303303
return res.Stdout, nil
304304
}
305305

306+
// IsAspireHostProject returns true if the project at the given path has an MS Build Property named "IsAspireHost" which is
307+
// set to true or has a ProjectCapability named "Aspire".
308+
func (cli *Cli) IsAspireHostProject(ctx context.Context, projectPath string) (bool, error) {
309+
runArgs := newDotNetRunArgs("msbuild", projectPath, "--getProperty:IsAspireHost", "--getItem:ProjectCapability")
310+
res, err := cli.commandRunner.Run(ctx, runArgs)
311+
if err != nil {
312+
return false, fmt.Errorf("running dotnet msbuild on project '%s': %w", projectPath, err)
313+
}
314+
315+
var result struct {
316+
Properties struct {
317+
IsAspireHost string `json:"IsAspireHost"`
318+
} `json:"Properties"`
319+
Items struct {
320+
ProjectCapability []struct {
321+
Identity string `json:"Identity"`
322+
} `json:"ProjectCapability"`
323+
} `json:"Items"`
324+
}
325+
326+
if err := json.Unmarshal([]byte(res.Stdout), &result); err != nil {
327+
return false, fmt.Errorf("unmarshal dotnet msbuild output: %w", err)
328+
}
329+
330+
hasAspireCapability := false
331+
332+
for _, capability := range result.Items.ProjectCapability {
333+
if capability.Identity == "Aspire" {
334+
hasAspireCapability = true
335+
break
336+
}
337+
}
338+
339+
return result.Properties.IsAspireHost == "true" || hasAspireCapability, nil
340+
}
341+
306342
func NewCli(commandRunner exec.CommandRunner) *Cli {
307343
return &Cli{
308344
commandRunner: commandRunner,

0 commit comments

Comments
 (0)