Skip to content

Commit 0925006

Browse files
yasmoradiCopilot
andcommitted
feat(templates): simplify Docker to Azure managed services switch in Aspire AppHost bitfoundation#12366
- Add IDistributedApplicationBuilderExtensions with encapsulated resource methods (AddRedisCache, AddRedisPersistent, AddSqlServer, AddPostgreSQL, AddMySql, AddSqlite, AddAzureStorage, AddMaui) using Azure-native hosting packages wrapped with RunAsContainer()/RunAsEmulator() for local dev - Simplify Program.cs to clean one-liner calls; removing RunAsContainer() on any method is now sufficient to switch to the real Azure managed service - Replace Docker-only NuGet packages with Azure-capable equivalents: Aspire.Hosting.SqlServer -> Aspire.Hosting.Azure.Sql Aspire.Hosting.PostgreSQL -> Aspire.Hosting.Azure.PostgreSQL Aspire.Hosting.Redis + Aspire.Hosting.Azure.Redis (both referenced) - Bump Aspire package versions to 13.3.4, Magick.NET to 14.13.1 - Update AppHost SDK to 13.3.3 - Remove now-redundant 'Going Production' docs section Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent ae44112 commit 0925006

6 files changed

Lines changed: 219 additions & 166 deletions

File tree

src/Templates/Boilerplate/Bit.Boilerplate/.docs/20- .NET Aspire.md

Lines changed: 0 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -333,70 +333,6 @@ This ensures **consistency** between development and production environments!
333333

334334
---
335335

336-
## ☁️ Going Production: Switching to Azure Managed Services
337-
338-
While local containers (Docker) are perfect for development, for production, you could switch to fully managed Azure services to ensure scalability, security, and high availability.
339-
340-
.NET Aspire makes this transition seamless by swapping the hosting resources in your `AppHost/Program.cs` without changing your application code.
341-
342-
### 1. Prerequisite Packages
343-
344-
Ensure you have the Azure integration packages installed in your **AppHost** project:
345-
346-
```xml
347-
<PackageReference Include="Aspire.Hosting.Azure.Sql" />
348-
<PackageReference Include="Aspire.Hosting.Azure.Redis" />
349-
<PackageReference Include="Aspire.Hosting.Azure.PostgreSQL" />
350-
351-
```
352-
353-
### 2. Updating AppHost (`Program.cs`)
354-
355-
Replace your local container definitions with Azure resources.
356-
357-
#### 🛢️ Azure SQL Database
358-
359-
Instead of `AddSqlServer` (container), use `AddAzureSqlServer`:
360-
361-
```csharp
362-
// ❌ Local Container
363-
// var sqlServer = builder.AddSqlServer("sqlserver");
364-
365-
// ✅ Azure Managed Service
366-
var sqlServer = builder.AddAzureSqlServer("sqlserver")
367-
.AddDatabase("sqldb");
368-
369-
```
370-
371-
#### ⚡ Azure Cache for Redis
372-
373-
Instead of `AddRedis` (container), use `AddAzureRedis`:
374-
375-
```csharp
376-
// ❌ Local Container
377-
// var redis = builder.AddRedis("redis");
378-
379-
// ✅ Azure Managed Service
380-
var redis = builder.AddAzureRedis("redis");
381-
382-
```
383-
384-
#### 🐘 Azure Database for PostgreSQL (Flexible Server)
385-
386-
To leverage high-performance vector search in production, use Azure PostgreSQL Flexible Server.
387-
388-
```csharp
389-
// ❌ Local Container
390-
// var postgres = builder.AddPostgres("postgres");
391-
392-
// ✅ Azure Managed Service
393-
var postgres = builder.AddAzurePostgresFlexibleServer("postgres")
394-
.AddDatabase("pgdb");
395-
396-
```
397-
398-
---
399-
400336
## Additional Resources
401337

402338
- 📚 **Official Documentation**: https://aspire.dev

src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -60,15 +60,15 @@
6060
<!--/+:msbuild-conditional:noEmit -->
6161
<PackageVersion Condition="'$(signalR)' == 'true' OR '$(signalR)' == ''" Include="ModelContextProtocol.AspNetCore" Version="1.3.0" />
6262
<PackageVersion Condition=" '$(aspire)' == 'true' OR '$(aspire)' == '' " Include="Aspire.Hosting.Maui" Version="13.3.3-preview.1.26264.13" />
63-
<PackageVersion Condition=" '$(aspire)' == 'true' OR '$(aspire)' == '' " Include="Aspire.Hosting.Testing" Version="13.3.3" />
63+
<PackageVersion Condition=" '$(aspire)' == 'true' OR '$(aspire)' == '' " Include="Aspire.Hosting.Testing" Version="13.3.4" />
6464
<PackageVersion Condition=" '$(aspire)' == 'true' OR '$(aspire)' == '' " Include="CommunityToolkit.Aspire.Hosting.MailPit" Version="13.3.0" />
65-
<PackageVersion Condition=" '$(aspire)' == 'true' OR '$(aspire)' == '' " Include="Aspire.Hosting.DevTunnels" Version="13.3.3" />
65+
<PackageVersion Condition=" '$(aspire)' == 'true' OR '$(aspire)' == '' " Include="Aspire.Hosting.DevTunnels" Version="13.3.4" />
6666
<PackageVersion Condition=" ('$(aspire)' == 'true' OR '$(aspire)' == '') AND ('$(database)' == 'Sqlite' OR '$(database)' == '') " Include="CommunityToolkit.Aspire.Hosting.Sqlite" Version="13.3.0" />
67-
<PackageVersion Condition=" ('$(aspire)' == 'true' OR '$(aspire)' == '') AND ('$(database)' == 'SqlServer' OR '$(database)' == '') " Include="Aspire.Hosting.SqlServer" Version="13.3.3" />
67+
<PackageVersion Condition=" ('$(aspire)' == 'true' OR '$(aspire)' == '') AND ('$(database)' == 'SqlServer' OR '$(database)' == '') " Include="Aspire.Hosting.Azure.Sql" Version="13.3.4" />
6868
<PackageVersion Condition=" ('$(aspire)' == 'true' OR '$(aspire)' == '') AND ('$(database)' == 'SqlServer' OR '$(database)' == '') " Include="CommunityToolkit.Aspire.Hosting.SqlServer.Extensions" Version="13.3.0" />
69-
<PackageVersion Condition=" ('$(aspire)' == 'true' OR '$(aspire)' == '') AND ('$(database)' == 'PostgreSQL' OR '$(database)' == '') " Include="Aspire.Hosting.PostgreSQL" Version="13.3.3" />
70-
<PackageVersion Condition=" ('$(aspire)' == 'true' OR '$(aspire)' == '') AND ('$(database)' == 'MySql' OR '$(database)' == '') " Include="Aspire.Hosting.MySql" Version="13.3.3" />
71-
<PackageVersion Condition=" ('$(aspire)' == 'true' OR '$(aspire)' == '') AND ('$(filesStorage)' == 'AzureBlobStorage' OR '$(filesStorage)' == '') " Include="Aspire.Hosting.Azure.Storage" Version="13.3.3" />
69+
<PackageVersion Condition=" ('$(aspire)' == 'true' OR '$(aspire)' == '') AND ('$(database)' == 'PostgreSQL' OR '$(database)' == '') " Include="Aspire.Hosting.Azure.PostgreSQL" Version="13.3.4" />
70+
<PackageVersion Condition=" ('$(aspire)' == 'true' OR '$(aspire)' == '') AND ('$(database)' == 'MySql' OR '$(database)' == '') " Include="Aspire.Hosting.MySql" Version="13.3.4" />
71+
<PackageVersion Condition=" ('$(aspire)' == 'true' OR '$(aspire)' == '') AND ('$(filesStorage)' == 'AzureBlobStorage' OR '$(filesStorage)' == '') " Include="Aspire.Hosting.Azure.Storage" Version="13.3.4" />
7272
<PackageVersion Condition=" ('$(aspire)' == 'true' OR '$(aspire)' == '') AND ('$(filesStorage)' == 'S3' OR '$(filesStorage)' == '') " Include="CommunityToolkit.Aspire.Hosting.Minio" Version="13.3.0" />
7373
<PackageVersion Condition=" ('$(aspire)' == 'true' OR '$(aspire)' == '')" Include="Aspire.Hosting.Keycloak" Version="13.3.3-preview.1.26264.13" />
7474
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="10.6.0" />
@@ -110,8 +110,9 @@
110110
<PackageVersion Condition=" '$(filesStorage)' == 'S3' OR '$(filesStorage)' == '' " Include="FluentStorage.AWS" Version="6.0.3" />
111111
<PackageVersion Condition=" '$(appInsights)' == 'true' OR '$(appInsights)' == '' " Include="Azure.Monitor.OpenTelemetry.AspNetCore" Version="1.5.0" />
112112
<PackageVersion Condition=" '$(appInsights)' == 'true' OR '$(appInsights)' == '' " Include="Azure.Monitor.OpenTelemetry.Profiler" Version="1.0.1-beta.1" />
113-
<PackageVersion Condition="'$(redis)' == 'true' OR '$(redis)' == ''" Include="Aspire.Hosting.Redis" Version="13.3.3" />
114-
<PackageVersion Condition="'$(redis)' == 'true' OR '$(redis)' == ''" Include="Aspire.StackExchange.Redis" Version="13.3.3" />
113+
<PackageVersion Condition="'$(redis)' == 'true' OR '$(redis)' == ''" Include="Aspire.Hosting.Redis" Version="13.3.4" />
114+
<PackageVersion Condition="'$(redis)' == 'true' OR '$(redis)' == ''" Include="Aspire.Hosting.Azure.Redis" Version="13.3.4" />
115+
<PackageVersion Condition="'$(redis)' == 'true' OR '$(redis)' == ''" Include="Aspire.StackExchange.Redis" Version="13.3.4" />
115116
<PackageVersion Condition="'$(redis)' == 'true' OR '$(redis)' == ''" Include="ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis" Version="2.6.0" />
116117
<PackageVersion Condition="'$(redis)' == 'true' OR '$(redis)' == ''" Include="DistributedLock.Redis" Version="1.1.1" />
117118
<PackageVersion Condition="'$(redis)' == 'true' OR '$(redis)' == ''" Include="Hangfire.Redis.StackExchange" Version="1.12.0" />
@@ -121,7 +122,7 @@
121122
<!--/-:msbuild-conditional:noEmit -->
122123
<PackageVersion Include="Humanizer" Version="3.0.10" />
123124
<PackageVersion Include="QRCoder" Version="1.8.0" />
124-
<PackageVersion Include="Magick.NET-Q16-AnyCPU" Version="14.13.0" />
125+
<PackageVersion Include="Magick.NET-Q16-AnyCPU" Version="14.13.1" />
125126
<PackageVersion Include="FluentEmail.Smtp" Version="3.0.2" />
126127
<PackageVersion Include="FluentStorage" Version="6.0.4" />
127128
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.8" />

src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.AppHost/Boilerplate.Server.AppHost.csproj

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Aspire.AppHost.Sdk/13.3.2">
1+
<Project Sdk="Aspire.AppHost.Sdk/13.3.3">
22

33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
@@ -14,11 +14,12 @@
1414
<PackageReference Include="Aspire.Hosting.Maui" />
1515
<PackageReference Condition=" '$(database)' == 'MySql' OR '$(database)' == '' " Include="Aspire.Hosting.MySql" />
1616
<PackageReference Condition="'$(redis)' == 'true' OR '$(redis)' == ''" Include="Aspire.Hosting.Redis" />
17+
<PackageReference Condition="'$(redis)' == 'true' OR '$(redis)' == ''" Include="Aspire.Hosting.Azure.Redis" />
1718
<PackageReference Include="CommunityToolkit.Aspire.Hosting.MailPit" />
1819
<PackageReference Condition=" '$(database)' == 'Sqlite' OR '$(database)' == '' " Include="CommunityToolkit.Aspire.Hosting.Sqlite" />
19-
<PackageReference Condition=" '$(database)' == 'SqlServer' OR '$(database)' == ''" Include="Aspire.Hosting.SqlServer" />
20+
<PackageReference Condition=" '$(database)' == 'SqlServer' OR '$(database)' == ''" Include="Aspire.Hosting.Azure.Sql" />
2021
<PackageReference Condition=" '$(database)' == 'SqlServer' OR '$(database)' == ''" Include="CommunityToolkit.Aspire.Hosting.SqlServer.Extensions" />
21-
<PackageReference Condition=" '$(database)' == 'PostgreSQL' OR '$(database)' == '' " Include="Aspire.Hosting.PostgreSQL" />
22+
<PackageReference Condition=" '$(database)' == 'PostgreSQL' OR '$(database)' == '' " Include="Aspire.Hosting.Azure.PostgreSQL" />
2223
<PackageReference Condition=" '$(filesStorage)' == 'AzureBlobStorage' OR '$(filesStorage)' == '' " Include="Aspire.Hosting.Azure.Storage" />
2324
<PackageReference Condition=" '$(filesStorage)' == 'S3' OR '$(filesStorage)' == '' " Include="CommunityToolkit.Aspire.Hosting.Minio" />
2425
</ItemGroup>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
//+:cnd:noEmit
2+
using Aspire.Hosting.Maui;
3+
using Aspire.Hosting.Azure;
4+
using Aspire.Hosting.DevTunnels;
5+
using Azure.Provisioning.RedisEnterprise;
6+
7+
namespace Aspire.Hosting;
8+
9+
public static class IDistributedApplicationBuilderExtensions
10+
{
11+
//#if (redis == true)
12+
/// <summary>
13+
/// Adds a Redis instance configured for FusionCache hybrid caching (L2 cache) and SignalR backplane.
14+
/// No persistence is needed for this cache instance.
15+
/// </summary>
16+
public static IResourceBuilder<AzureManagedRedisResource> AddRedisCache(this IDistributedApplicationBuilder builder)
17+
{
18+
return builder.AddAzureManagedRedis("redis-cache")
19+
.RunAsContainer(redis => // Remove this RunAsContainer and related configuration to use actual Azure Redis instance
20+
{
21+
redis.WithRedisInsight()
22+
.WithRedisCommander()
23+
.WithImage("redis/redis-stack", "latest");
24+
});
25+
}
26+
27+
/// <summary>
28+
/// Adds a Redis instance configured for Hangfire background jobs and distributed locking.
29+
/// This instance uses AOF persistence for durability.
30+
/// </summary>
31+
public static IResourceBuilder<AzureManagedRedisResource> AddRedisPersistent(this IDistributedApplicationBuilder builder)
32+
{
33+
return builder.AddAzureManagedRedis("redis-persistent")
34+
.RunAsContainer(redis => // Remove this RunAsContainer and related configuration to use actual Azure Redis instance
35+
{
36+
redis.WithRedisInsight()
37+
.WithRedisCommander()
38+
.WithImage("redis/redis-stack", "latest")
39+
.WithArgs(
40+
"--appendonly", "yes", // Enable AOF (Append only file) for data durability
41+
"--appendfsync", "always", // Sync to disk on every write for maximum durability. Temporarily disable it programmatically using C# code during bulk operations if needed.
42+
"--save", "", // Disables RDB snapshots
43+
"--maxmemory-policy", "noeviction" // Raise error when memory limit is reached instead of evicting keys
44+
);
45+
})
46+
.ConfigureInfrastructure(infra =>
47+
{
48+
var db = infra.GetProvisionableResources()
49+
.OfType<RedisEnterpriseDatabase>()
50+
.Single();
51+
52+
// --appendonly yes + --appendfsync always
53+
db.Persistence = new()
54+
{
55+
IsAofEnabled = true,
56+
AofFrequency = PersistenceSettingAofFrequency.Always,
57+
IsRdbEnabled = false // --save ""
58+
};
59+
60+
// --maxmemory-policy noeviction
61+
db.EvictionPolicy = RedisEnterpriseEvictionPolicy.NoEviction;
62+
});
63+
}
64+
//#endif
65+
66+
//#if (database == "SqlServer")
67+
/// <summary>
68+
/// Adds a SQL Server instance with DbGate management UI and a database named <c>mssqldb</c>.
69+
/// Uses SQL Server 2025 which supports embedded vector search.
70+
/// </summary>
71+
public static IResourceBuilder<AzureSqlDatabaseResource> AddSqlServer(this IDistributedApplicationBuilder builder)
72+
{
73+
return builder.AddAzureSqlServer("sqlserver")
74+
.RunAsContainer(sqlServer => // Remove this RunAsContainer and related configuration to use actual Azure SQL Server instance
75+
{
76+
sqlServer.WithDbGate(config => config.WithDataVolume())
77+
.WithDataVolume()
78+
.WithImage("mssql/server", "2025-latest"); // Sql server 2025 supports embedded vector search.
79+
})
80+
.AddDatabase("mssqldb");
81+
}
82+
//#endif
83+
84+
//#if (database == "PostgreSql")
85+
/// <summary>
86+
/// Adds a PostgreSQL Server instance with pgAdmin and a database named <c>postgresdb</c>.
87+
/// Uses pgvector (pg18) image which supports embedded vector search.
88+
/// </summary>
89+
public static IResourceBuilder<AzurePostgresFlexibleServerDatabaseResource> AddPostgreSQL(this IDistributedApplicationBuilder builder)
90+
{
91+
return builder.AddAzurePostgresFlexibleServer("postgresserver")
92+
.RunAsContainer(postgresDatabase =>
93+
{
94+
postgresDatabase.WithPgAdmin()
95+
.WithV18DataVolume()
96+
.WithOptimizedSetup()
97+
.WithImage("pgvector/pgvector", "pg18"); // pgvector supports embedded vector search.
98+
})
99+
.AddDatabase("postgresdb");
100+
}
101+
//#endif
102+
103+
//#if (database == "MySql")
104+
/// <summary>
105+
/// Adds a MySQL server instance with phpMyAdmin and a database named <c>mysqldb</c>.
106+
/// </summary>
107+
public static IResourceBuilder<MySqlDatabaseResource> AddMySql(this IDistributedApplicationBuilder builder)
108+
{
109+
return builder.AddMySql("mysqlserver")
110+
.WithPhpMyAdmin()
111+
.WithDataVolume()
112+
.AddDatabase("mysqldb");
113+
}
114+
//#endif
115+
116+
//#if (database == "Sqlite")
117+
/// <summary>
118+
/// Adds a SQLite database instance with a web-based management UI.
119+
/// </summary>
120+
public static IResourceBuilder<SqliteResource> AddSqlite(this IDistributedApplicationBuilder builder)
121+
{
122+
return builder.AddSqlite("sqlite", databaseFileName: "BoilerplateDb.db")
123+
.WithSqliteWeb();
124+
}
125+
//#endif
126+
127+
//#if (filesStorage == "AzureBlobStorage")
128+
public static IResourceBuilder<AzureBlobStorageResource> AddAzureStorage(this IDistributedApplicationBuilder builder)
129+
{
130+
return builder.AddAzureStorage("storage")
131+
.RunAsEmulator(azurite => // Remove this RunAsEmulator and related configuration to use actual Azure Blob Storage instance
132+
{
133+
azurite
134+
.WithDataVolume();
135+
})
136+
.AddBlobs("azureblobstorage");
137+
}
138+
//#endif
139+
140+
/// <summary>
141+
/// Adds the .NET MAUI Blazor Hybrid project and configures it for all supported device targets
142+
/// (Windows, macOS Catalyst, iOS Device, iOS Simulator, Android Device, Android Emulator).
143+
/// Uses dev tunnels for OpenTelemetry data collection on mobile/remote targets.
144+
/// </summary>
145+
public static IResourceBuilder<MauiProjectResource> AddMaui(
146+
this IDistributedApplicationBuilder builder,
147+
IResourceBuilder<ProjectResource> serverWebProject,
148+
IResourceBuilder<DevTunnelResource> tunnel)
149+
{
150+
var mauiapp = builder.AddMauiProject("mauiapp", @"../../Client/Boilerplate.Client.Maui/Boilerplate.Client.Maui.csproj");
151+
152+
if (OperatingSystem.IsWindows())
153+
{
154+
mauiapp.AddWindowsDevice()
155+
.WithExplicitStart()
156+
.WithReference(serverWebProject);
157+
}
158+
159+
if (OperatingSystem.IsMacOS())
160+
{
161+
mauiapp.AddMacCatalystDevice()
162+
.WithExplicitStart()
163+
.WithReference(serverWebProject);
164+
}
165+
166+
if (OperatingSystem.IsMacOS())
167+
{
168+
// Windows supports iOS Simulator and Physical devices if there's a mac connected to network, but the following runners only work on macOS for now.
169+
170+
mauiapp.AddiOSDevice()
171+
.WithExplicitStart()
172+
.WithOtlpDevTunnel() // Required for OpenTelemetry data collection
173+
.WithReference(serverWebProject, tunnel);
174+
175+
mauiapp.AddiOSSimulator()
176+
.WithExplicitStart()
177+
.WithOtlpDevTunnel() // Required for OpenTelemetry data collection
178+
.WithReference(serverWebProject, tunnel);
179+
}
180+
181+
mauiapp.AddAndroidDevice()
182+
.WithExplicitStart()
183+
.WithOtlpDevTunnel() // Required for OpenTelemetry data collection
184+
.WithReference(serverWebProject, tunnel);
185+
186+
mauiapp.AddAndroidEmulator()
187+
.WithExplicitStart()
188+
.WithOtlpDevTunnel() // Required for OpenTelemetry data collection
189+
.WithReference(serverWebProject, tunnel);
190+
191+
return mauiapp;
192+
}
193+
}

0 commit comments

Comments
 (0)