Skip to content

Commit 4a51e7d

Browse files
Use PathSha for deployment state and ProjectNameSha for Azure Functions (#12128)
* Initial plan * Implement PathSha256 and ProjectNameSha256 for deployment state and Azure Functions Co-authored-by: captainsafia <[email protected]> * Fix AppHost:Sha256 backward compatibility for volume naming Co-authored-by: captainsafia <[email protected]> * Add tests for PathSha and ProjectNameSha behavior Co-authored-by: captainsafia <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: captainsafia <[email protected]>
1 parent 58f923b commit 4a51e7d

File tree

6 files changed

+118
-18
lines changed

6 files changed

+118
-18
lines changed

src/Aspire.Hosting.Azure.Functions/AzureFunctionsProjectResourceExtensions.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ public static class AzureFunctionsProjectResourceExtensions
2121
/// is a combination of this prefix, a hash of the AppHost project name, and the name of the
2222
/// resource group associated with the deployment. We want to keep the total number of characters
2323
/// in the name under 24 characters to avoid truncation by Azure and allow
24-
/// for unique enough identifiers.
24+
/// for unique enough identifiers. The hash is based on the project name (not path) to ensure
25+
/// stable naming across deployments.
2526
/// </remarks>
2627
internal const string DefaultAzureFunctionsHostStorageName = "funcstorage";
2728

@@ -249,7 +250,8 @@ public static IResourceBuilder<AzureFunctionsProjectResource> WithReference<TSou
249250

250251
private static string CreateDefaultStorageName(this IDistributedApplicationBuilder builder)
251252
{
252-
var applicationHash = builder.Configuration["AppHost:Sha256"]![..5].ToLowerInvariant();
253+
// Use ProjectNameSha256 for stable naming across deployments regardless of path
254+
var applicationHash = builder.Configuration["AppHost:ProjectNameSha256"]![..5].ToLowerInvariant();
253255
return $"{DefaultAzureFunctionsHostStorageName}{applicationHash}";
254256
}
255257
}

src/Aspire.Hosting.Azure/AzureEnvironmentResourceExtensions.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,8 @@ public static IResourceBuilder<AzureEnvironmentResource> WithResourceGroup(
9595

9696
private static string CreateDefaultAzureEnvironmentName(this IDistributedApplicationBuilder builder)
9797
{
98-
var applicationHash = builder.Configuration["AppHost:Sha256"]?[..5].ToLowerInvariant();
98+
// Use ProjectNameSha256 for stable naming across deployments
99+
var applicationHash = builder.Configuration["AppHost:ProjectNameSha256"]?[..5].ToLowerInvariant();
99100
return $"azure{applicationHash}";
100101
}
101102
}

src/Aspire.Hosting/DistributedApplicationBuilder.cs

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -220,39 +220,60 @@ public DistributedApplicationBuilder(DistributedApplicationOptions options)
220220
_executionContextOptions = BuildExecutionContextOptions();
221221
ExecutionContext = new DistributedApplicationExecutionContext(_executionContextOptions);
222222

223-
// Conditionally configure AppHostSha based on execution context. For local scenarios, we want to
224-
// account for the path the AppHost is running from to disambiguate between different projects
225-
// with the same name as seen in https://github.com/dotnet/aspire/issues/5413. For publish scenarios,
226-
// we want to use a stable hash based only on the project name.
227-
string appHostSha;
223+
// Compute both PathSha and ProjectNameSha to support different use cases:
224+
// - PathSha: For disambiguating projects with the same name in different locations (deployment state)
225+
// - ProjectNameSha: For stable naming across deployments regardless of path (Azure Functions, Azure environments)
226+
string appHostPathSha;
227+
string appHostProjectNameSha;
228+
string appHostSha; // Legacy value, computed based on mode
228229

229230
// Check if AppHostSha is already configured (e.g., for testing scenarios)
230231
var configuredAppHostSha = _innerBuilder.Configuration["AppHostSha"];
231232
if (!string.IsNullOrEmpty(configuredAppHostSha))
232233
{
234+
// For backward compatibility with tests
235+
appHostPathSha = configuredAppHostSha;
236+
appHostProjectNameSha = configuredAppHostSha;
233237
appHostSha = configuredAppHostSha;
234238
}
235-
else if (ExecutionContext.IsPublishMode)
236-
{
237-
var appHostNameShaBytes = SHA256.HashData(Encoding.UTF8.GetBytes(appHostName));
238-
appHostSha = Convert.ToHexString(appHostNameShaBytes);
239-
}
240239
else
241240
{
242-
// Normalize the casing of AppHostPath
243-
var appHostShaBytes = SHA256.HashData(Encoding.UTF8.GetBytes(AppHostPath.ToLowerInvariant()));
244-
appHostSha = Convert.ToHexString(appHostShaBytes);
241+
// Normalize the casing of AppHostPath and compute PathSha
242+
var appHostPathShaBytes = SHA256.HashData(Encoding.UTF8.GetBytes(AppHostPath.ToLowerInvariant()));
243+
appHostPathSha = Convert.ToHexString(appHostPathShaBytes);
244+
245+
// Compute ProjectNameSha
246+
var appHostProjectNameShaBytes = SHA256.HashData(Encoding.UTF8.GetBytes(appHostName));
247+
appHostProjectNameSha = Convert.ToHexString(appHostProjectNameShaBytes);
248+
249+
// For backward compatibility, AppHost:Sha256 uses the old logic:
250+
// - Publish mode: ProjectNameSha (stable across paths)
251+
// - Run mode: PathSha (disambiguates by path)
252+
if (ExecutionContext.IsPublishMode)
253+
{
254+
appHostSha = appHostProjectNameSha;
255+
}
256+
else
257+
{
258+
appHostSha = appHostPathSha;
259+
}
245260
}
261+
246262
_innerBuilder.Configuration.AddInMemoryCollection(new Dictionary<string, string?>
247263
{
264+
// PathSha for deployment state (path-based disambiguation)
265+
["AppHost:PathSha256"] = appHostPathSha,
266+
// ProjectNameSha for Azure Functions and Azure environments (stable naming)
267+
["AppHost:ProjectNameSha256"] = appHostProjectNameSha,
268+
// Legacy Sha256 for backward compatibility (mode-dependent)
248269
["AppHost:Sha256"] = appHostSha
249270
});
250271

251272
// Load deployment state early in the configuration chain if in publish mode
252273
// This must happen before command line args are added so they can override saved state
253274
if (ExecutionContext.IsPublishMode)
254275
{
255-
LoadDeploymentState(appHostSha);
276+
LoadDeploymentState(appHostPathSha);
256277
}
257278

258279
// exec

src/Aspire.Hosting/Publishing/Internal/FileDeploymentStateManager.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ public sealed class FileDeploymentStateManager(
3131

3232
private string? GetDeploymentStatePath()
3333
{
34-
var appHostSha = configuration["AppHost:Sha256"];
34+
// Use PathSha256 for deployment state to disambiguate projects with the same name in different locations
35+
var appHostSha = configuration["AppHost:PathSha256"];
3536
if (string.IsNullOrEmpty(appHostSha))
3637
{
3738
return null;

tests/Aspire.Hosting.Azure.Tests/AzureFunctionsTests.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@ public async Task AddAzureFunctionsProject_CanGetStorageManifestSuccessfully()
277277

278278
// hardcoded sha256 to make the storage name deterministic
279279
builder.Configuration["AppHost:Sha256"] = "634f8";
280+
builder.Configuration["AppHost:ProjectNameSha256"] = "634f8";
280281
var project = builder.AddAzureFunctionsProject<TestProjectWithHttpsNoPort>("funcapp");
281282

282283
var app = builder.Build();
@@ -307,6 +308,7 @@ public async Task AddAzureFunctionsProject_WorksWithAddAzureContainerAppsInfrast
307308

308309
// hardcoded sha256 to make the storage name deterministic
309310
builder.Configuration["AppHost:Sha256"] = "634f8";
311+
builder.Configuration["AppHost:ProjectNameSha256"] = "634f8";
310312
var funcApp = builder.AddAzureFunctionsProject<TestProjectWithHttpsNoPort>("funcapp");
311313

312314
var app = builder.Build();
@@ -335,6 +337,7 @@ public async Task AddAzureFunctionsProject_WorksWithAddAzureContainerAppsInfrast
335337
// hardcoded sha256 to make the storage name deterministic
336338
var storage = builder.AddAzureStorage("my-own-storage").RunAsEmulator();
337339
builder.Configuration["AppHost:Sha256"] = "634f8";
340+
builder.Configuration["AppHost:ProjectNameSha256"] = "634f8";
338341
builder.AddAzureFunctionsProject<TestProjectWithHttpsNoPort>("funcapp")
339342
.WithHostStorage(storage);
340343

@@ -362,6 +365,7 @@ public async Task AddAzureFunctionsProject_WorksWithAddAzureContainerAppsInfrast
362365
// hardcoded sha256 to make the storage name deterministic
363366
var storage = builder.AddAzureStorage("my-own-storage").RunAsEmulator();
364367
builder.Configuration["AppHost:Sha256"] = "634f8";
368+
builder.Configuration["AppHost:ProjectNameSha256"] = "634f8";
365369
builder.AddAzureFunctionsProject<TestProjectWithHttpsNoPort>("funcapp")
366370
.WithHostStorage(storage)
367371
.WithRoleAssignments(storage, StorageBuiltInRole.StorageBlobDataOwner);
@@ -390,6 +394,7 @@ public async Task MultipleAddAzureFunctionsProject_WorksWithAddAzureContainerApp
390394
// hardcoded sha256 to make the storage name deterministic
391395
var storage = builder.AddAzureStorage("my-own-storage").RunAsEmulator();
392396
builder.Configuration["AppHost:Sha256"] = "634f8";
397+
builder.Configuration["AppHost:ProjectNameSha256"] = "634f8";
393398
builder.AddAzureFunctionsProject<TestProjectWithHttpsNoPort>("funcapp")
394399
.WithHostStorage(storage)
395400
.WithRoleAssignments(storage, StorageBuiltInRole.StorageBlobDataOwner);

tests/Aspire.Hosting.Tests/DistributedApplicationBuilderTests.cs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,76 @@ public void Build_DuplicateResourceNames_SameCasing_Error()
170170
Assert.Equal("Multiple resources with the name 'Test'. Resource names are case-insensitive.", ex.Message);
171171
}
172172

173+
[Fact]
174+
public void PathShaAndProjectNameShaBothAvailable()
175+
{
176+
var appBuilder = DistributedApplication.CreateBuilder();
177+
178+
var pathSha = appBuilder.Configuration["AppHost:PathSha256"];
179+
var projectNameSha = appBuilder.Configuration["AppHost:ProjectNameSha256"];
180+
var legacySha = appBuilder.Configuration["AppHost:Sha256"];
181+
182+
// Verify all three SHA values are available
183+
Assert.NotNull(pathSha);
184+
Assert.NotNull(projectNameSha);
185+
Assert.NotNull(legacySha);
186+
187+
// In run mode, legacy SHA should equal PathSha
188+
Assert.False(appBuilder.ExecutionContext.IsPublishMode);
189+
Assert.Equal(pathSha, legacySha);
190+
}
191+
192+
[Fact]
193+
public void PathShaDiffersForDifferentPaths()
194+
{
195+
var options1 = new DistributedApplicationOptions
196+
{
197+
ProjectDirectory = "/home/user/project1",
198+
ProjectName = "TestApp",
199+
Args = []
200+
};
201+
202+
var options2 = new DistributedApplicationOptions
203+
{
204+
ProjectDirectory = "/home/user/project2",
205+
ProjectName = "TestApp", // Same name, different path
206+
Args = []
207+
};
208+
209+
var builder1 = (DistributedApplicationBuilder)DistributedApplication.CreateBuilder(options1);
210+
var builder2 = (DistributedApplicationBuilder)DistributedApplication.CreateBuilder(options2);
211+
212+
var pathSha1 = builder1.Configuration["AppHost:PathSha256"];
213+
var pathSha2 = builder2.Configuration["AppHost:PathSha256"];
214+
var projectNameSha1 = builder1.Configuration["AppHost:ProjectNameSha256"];
215+
var projectNameSha2 = builder2.Configuration["AppHost:ProjectNameSha256"];
216+
217+
// PathSha should differ for different paths
218+
Assert.NotEqual(pathSha1, pathSha2);
219+
220+
// ProjectNameSha should be the same for same project name
221+
Assert.Equal(projectNameSha1, projectNameSha2);
222+
}
223+
224+
[Fact]
225+
public void LegacyShaUsesProjectNameShaInPublishMode()
226+
{
227+
var appBuilder = DistributedApplication.CreateBuilder(["--publisher", "manifest"]);
228+
229+
var pathSha = appBuilder.Configuration["AppHost:PathSha256"];
230+
var projectNameSha = appBuilder.Configuration["AppHost:ProjectNameSha256"];
231+
var legacySha = appBuilder.Configuration["AppHost:Sha256"];
232+
233+
// Verify all three SHA values are available
234+
Assert.NotNull(pathSha);
235+
Assert.NotNull(projectNameSha);
236+
Assert.NotNull(legacySha);
237+
238+
// In publish mode, legacy SHA should equal ProjectNameSha
239+
Assert.True(appBuilder.ExecutionContext.IsPublishMode);
240+
Assert.Equal(projectNameSha, legacySha);
241+
}
242+
173243
private sealed class TestResource : IResource
174244
{
175245
public string Name => nameof(TestResource);

0 commit comments

Comments
 (0)