Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
5ad0d72
first pass at #8876
yorek Apr 19, 2025
c4d6734
introduced WithSku to manage database SKU
yorek Apr 21, 2025
d2257c1
Improved sku comparison code
yorek Apr 21, 2025
ff1fa57
changes as per comments
yorek Apr 21, 2025
4202efa
formatting improvements
yorek Apr 21, 2025
d090962
Update src/Aspire.Hosting.Azure.Sql/AzureSqlExtensions.cs
yorek Apr 22, 2025
9acc9ad
get is now public, added test
yorek Apr 22, 2025
177b074
Merge branch 'azuresqldb-8876' of https://github.com/yorek/aspire int…
yorek Apr 22, 2025
a6de371
Update src/Aspire.Hosting.Azure.Sql/AzureSqlExtensions.cs
yorek Apr 23, 2025
73d5043
Move private member up in the class
sebastienros Apr 24, 2025
e0d27f7
Update src/Aspire.Hosting.Azure.Sql/AzureSqlExtensions.cs
yorek Apr 26, 2025
d63a90c
Update src/Aspire.Hosting.Azure.Sql/README.md
yorek Apr 26, 2025
56acbd7
Update src/Aspire.Hosting.Azure.Sql/AzureSqlExtensions.cs
yorek Apr 26, 2025
93847b8
Update src/Aspire.Hosting.Azure.Sql/AzureSqlExtensions.cs
yorek Apr 26, 2025
d5b3c65
Update src/Aspire.Hosting.Azure.Sql/README.md
yorek Apr 26, 2025
a0119a3
Fix missing xml tag
sebastienros Apr 29, 2025
ee95da2
Merge branch 'dotnet:main' into azuresqldb-8876
yorek May 7, 2025
9ff69c1
removed WithSku and added WithDefaultAzureSku
yorek May 7, 2025
d99070d
Updated readme
yorek May 7, 2025
76e5648
Made UseDefaultAzureSku all internal
yorek May 7, 2025
89ecbfe
Fix AzureResourceOptionsCanBeConfigured test
sebastienros May 9, 2025
a891929
Fix AddContainerAppEnvironmentWorksWithSqlServer test
sebastienros May 9, 2025
de52a70
Merge remote-tracking branch 'origin/main' into azuresqldb-8876
sebastienros May 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/Aspire.Hosting.Azure.Sql/AzureSqlDatabaseResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ namespace Aspire.Hosting.Azure;
public class AzureSqlDatabaseResource(string name, string databaseName, AzureSqlServerResource parent)
: Resource(name), IResourceWithParent<AzureSqlServerResource>, IResourceWithConnectionString
{
/// <summary>
/// Free Azure SQL database offer
/// </summary>
internal const string FREE_SKU_NAME = "Free";

/// <summary>
/// SKU associated with the free offer
/// </summary>
internal const string FREE_DB_SKU = "GP_S_Gen5_2";

/// <summary>
/// Gets the parent Azure SQL Database (server) resource.
/// </summary>
Expand All @@ -32,6 +42,16 @@ public class AzureSqlDatabaseResource(string name, string databaseName, AzureSql
/// </summary>
public string DatabaseName { get; } = ThrowIfNullOrEmpty(databaseName);

/// <summary>
/// Gets or Sets the database SKU name
/// </summary>
public string SkuName
{
get { return _skuName; }
internal set { ThrowIfNullOrEmpty(value); _skuName = value.Trim(); }
}
private string _skuName = FREE_SKU_NAME;

/// <summary>
/// Gets the inner SqlServerDatabaseResource resource.
///
Expand Down
92 changes: 75 additions & 17 deletions src/Aspire.Hosting.Azure.Sql/AzureSqlExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public static IResourceBuilder<AzureSqlServerResource> AddAzureSqlServer(this ID
var configureInfrastructure = (AzureResourceInfrastructure infrastructure) =>
{
var azureResource = (AzureSqlServerResource)infrastructure.AspireResource;
CreateSqlServer(infrastructure, builder, azureResource.Databases);
CreateSqlServer(infrastructure, builder, azureResource.AzureSqlDatabases);
};

var resource = new AzureSqlServerResource(name, configureInfrastructure);
Expand All @@ -108,8 +108,9 @@ public static IResourceBuilder<AzureSqlDatabaseResource> AddDatabase(this IResou

var azureResource = builder.Resource;
var azureSqlDatabase = new AzureSqlDatabaseResource(name, databaseName, azureResource);
azureSqlDatabase.SkuName = AzureSqlDatabaseResource.FREE_SKU_NAME;

builder.Resource.AddDatabase(name, databaseName);
builder.Resource.AddDatabase(azureSqlDatabase);

if (azureResource.InnerResource is null)
{
Expand All @@ -127,6 +128,18 @@ public static IResourceBuilder<AzureSqlDatabaseResource> AddDatabase(this IResou
}
}

/// <summary>
/// Configures the Azure SQL Database to be deployed with the specified SKU
/// </summary>
/// <param name="builder">The builder for the Azure SQL resource.</param>
/// <param name="skuName">SKU of the database. If not provided, this defaults to the free database tier.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
public static IResourceBuilder<AzureSqlDatabaseResource> WithSku(this IResourceBuilder<AzureSqlDatabaseResource> builder, string skuName)
{
builder.Resource.SkuName = skuName;
return builder;
}

/// <summary>
/// Configures an Azure SQL Database (server) resource to run locally in a container.
/// </summary>
Expand Down Expand Up @@ -169,14 +182,14 @@ public static IResourceBuilder<AzureSqlServerResource> RunAsContainer(this IReso

azureResource.SetInnerResource(sqlContainer.Resource);

foreach (var database in azureResource.Databases)
foreach (var database in azureResource.AzureSqlDatabases)
{
if (!azureDatabases.TryGetValue(database.Key, out var existingDb))
{
throw new InvalidOperationException($"Could not find a {nameof(AzureSqlDatabaseResource)} with name {database.Key}.");
}

var innerDb = sqlContainer.AddDatabase(database.Key, database.Value);
var innerDb = sqlContainer.AddDatabase(database.Key, database.Value.DatabaseName);
existingDb.SetInnerResource(innerDb.Resource);
}

Expand All @@ -195,9 +208,64 @@ private static void RemoveAzureResources(IDistributedApplicationBuilder appBuild
}

private static void CreateSqlServer(
AzureResourceInfrastructure infrastructure,
AzureResourceInfrastructure infrastructure,
IDistributedApplicationBuilder distributedApplicationBuilder,
IReadOnlyDictionary<string, string> databases)
{
var sqlServer = CreateSqlServerResourceOnly(infrastructure, distributedApplicationBuilder);

foreach (var database in databases)
{
var bicepIdentifier = Infrastructure.NormalizeBicepIdentifier(database.Key);
var databaseName = database.Value;
var sqlDatabase = new SqlDatabase(bicepIdentifier)
{
Parent = sqlServer,
Name = databaseName,
};

sqlDatabase.Sku = new SqlSku() { Name = AzureSqlDatabaseResource.FREE_DB_SKU };
sqlDatabase.UseFreeLimit = true;
sqlDatabase.FreeLimitExhaustionBehavior = FreeLimitExhaustionBehavior.AutoPause;

infrastructure.Add(sqlDatabase);
}
}

private static void CreateSqlServer(
AzureResourceInfrastructure infrastructure,
IDistributedApplicationBuilder distributedApplicationBuilder,
IReadOnlyDictionary<string, AzureSqlDatabaseResource> databases)
{
var sqlServer = CreateSqlServerResourceOnly(infrastructure, distributedApplicationBuilder);

foreach (var database in databases)
{
var bicepIdentifier = Infrastructure.NormalizeBicepIdentifier(database.Key);
var databaseName = database.Value.DatabaseName;
var sqlDatabase = new SqlDatabase(bicepIdentifier)
{
Parent = sqlServer,
Name = databaseName,
};

if (string.Equals(database.Value.SkuName, AzureSqlDatabaseResource.FREE_SKU_NAME, StringComparison.InvariantCultureIgnoreCase))
{
sqlDatabase.Sku = new SqlSku() { Name = AzureSqlDatabaseResource.FREE_DB_SKU };
sqlDatabase.UseFreeLimit = true;
sqlDatabase.FreeLimitExhaustionBehavior = FreeLimitExhaustionBehavior.AutoPause;
}
else
{
sqlDatabase.Sku = new SqlSku() { Name = database.Value.SkuName };
}

infrastructure.Add(sqlDatabase);
}
}

private static SqlServer CreateSqlServerResourceOnly(AzureResourceInfrastructure infrastructure,
IDistributedApplicationBuilder distributedApplicationBuilder)
{
var azureResource = (AzureSqlServerResource)infrastructure.AspireResource;

Expand Down Expand Up @@ -263,22 +331,12 @@ private static void CreateSqlServer(
});
}

foreach (var databaseNames in databases)
{
var bicepIdentifier = Infrastructure.NormalizeBicepIdentifier(databaseNames.Key);
var databaseName = databaseNames.Value;
var sqlDatabase = new SqlDatabase(bicepIdentifier)
{
Parent = sqlServer,
Name = databaseName
};
infrastructure.Add(sqlDatabase);
}

infrastructure.Add(new ProvisioningOutput("sqlServerFqdn", typeof(string)) { Value = sqlServer.FullyQualifiedDomainName });

// We need to output name to externalize role assignments.
infrastructure.Add(new ProvisioningOutput("name", typeof(string)) { Value = sqlServer.Name });

return sqlServer;
}

internal static SqlServerAzureADAdministrator AddActiveDirectoryAdministrator(AzureResourceInfrastructure infra, SqlServer sqlServer, BicepValue<Guid> principalId, BicepValue<string> principalName)
Expand Down
18 changes: 13 additions & 5 deletions src/Aspire.Hosting.Azure.Sql/AzureSqlServerResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace Aspire.Hosting.Azure;
/// </summary>
public class AzureSqlServerResource : AzureProvisioningResource, IResourceWithConnectionString
{
private readonly Dictionary<string, string> _databases = new Dictionary<string, string>(StringComparers.ResourceName);
private readonly Dictionary<string, AzureSqlDatabaseResource> _databases = new Dictionary<string, AzureSqlDatabaseResource>(StringComparers.ResourceName);
private readonly bool _createdWithInnerResource;

/// <summary>
Expand Down Expand Up @@ -75,13 +75,21 @@ public ReferenceExpression ConnectionStringExpression
public override ResourceAnnotationCollection Annotations => InnerResource?.Annotations ?? base.Annotations;

/// <summary>
/// A dictionary where the key is the resource name and the value is the database name.
/// A dictionary where the key is the resource name and the value is the Azure SQL database resource.
/// </summary>
public IReadOnlyDictionary<string, string> Databases => _databases;
public IReadOnlyDictionary<string, AzureSqlDatabaseResource> AzureSqlDatabases => _databases;

internal void AddDatabase(string name, string databaseName)
/// <summary>
/// A dictionary where the key is the resource name and the value is the Azure SQL database name.
/// </summary>
public IReadOnlyDictionary<string, string> Databases => _databases.ToDictionary(
kvp => kvp.Key,
kvp => kvp.Value.DatabaseName
);

internal void AddDatabase(AzureSqlDatabaseResource db)
{
_databases.TryAdd(name, databaseName);
_databases.TryAdd(db.Name, db);
}

internal void SetInnerResource(SqlServerServerResource innerResource)
Expand Down
20 changes: 19 additions & 1 deletion src/Aspire.Hosting.Azure.Sql/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Aspire.Hosting.Azure.Sql library

Provides extension methods and resource definitions for a .NET Aspire AppHost to configure Azure SQL Server.
Provides extension methods and resource definitions for a .NET Aspire AppHost to configure Azure SQL DB.

## Getting started

Expand Down Expand Up @@ -54,6 +54,24 @@ The `WithReference` method configures a connection in the `MyService` project na
builder.AddSqlServerClient("sqldata");
```

## Azure SQL DB defaults

Unless otherwise specified, the Azure SQL DB created will be a 2vCores General Purpose Serverless database (GP_S_Gen5_2) with the free offer enabled.

Read more about the free offer here: [Deploy Azure SQL Database for free](https://learn.microsoft.com/en-us/azure/azure-sql/database/free-offer?view=azuresql)

The free offer is configured so that when the maximum usage limit is reached, the database is stopped to avoid incurring in unexpected costs.

If you want don't want to use the free offer and instead deploy the database with the service level of your choice, specify the SKU name when adding the database resource:

```csharp
var sql = builder.AddAzureSqlServer("sql")
.AddDatabase("db", "my-db-name").WithSku("HS_Gen5_2");

var myService = builder.AddProject<Projects.MyService>()
.WithReference(sql);
```

## Additional documentation

* https://learn.microsoft.com/dotnet/framework/data/adonet/sql/
Expand Down
Loading