+
+
+
+
+}
@if (!string.IsNullOrEmpty(additionalMessage))
{
diff --git a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.cs.xlf b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.cs.xlf
index e6cdaeced49..aece712f48e 100644
--- a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.cs.xlf
+++ b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.cs.xlf
@@ -14,7 +14,7 @@
- Trasování aplikace {0}
+ Trasa {0}{0} is an application name
diff --git a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.de.xlf b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.de.xlf
index 13e207b32f6..12569e21113 100644
--- a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.de.xlf
+++ b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.de.xlf
@@ -14,7 +14,7 @@
- {0} Ablaufverfolgungen
+ {0}-Ablaufverfolgung{0} is an application name
diff --git a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.es.xlf b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.es.xlf
index 325bab5ac4f..7beb527dbbd 100644
--- a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.es.xlf
+++ b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.es.xlf
@@ -14,7 +14,7 @@
- Seguimientos de {0}
+ Seguimiento de {0}{0} is an application name
diff --git a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.fr.xlf b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.fr.xlf
index d0b038e732a..010184f38e5 100644
--- a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.fr.xlf
+++ b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.fr.xlf
@@ -14,7 +14,7 @@
- Traces {0}
+ Trace {0}{0} is an application name
diff --git a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.it.xlf b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.it.xlf
index 417c6ca23b1..29a82efd30c 100644
--- a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.it.xlf
+++ b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.it.xlf
@@ -14,7 +14,7 @@
- {0} tracce
+ Traccia {0}{0} is an application name
diff --git a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.ja.xlf b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.ja.xlf
index 0e0a941b81e..b64d1d976f1 100644
--- a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.ja.xlf
+++ b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.ja.xlf
@@ -14,7 +14,7 @@
- {0} のトレース
+ {0} のトレース{0} is an application name
diff --git a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.ko.xlf b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.ko.xlf
index 4cd38c94572..0ef602cc869 100644
--- a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.ko.xlf
+++ b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.ko.xlf
@@ -14,7 +14,7 @@
- {0} 추적
+ {0} 추적{0} is an application name
diff --git a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.pl.xlf b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.pl.xlf
index 0801a6a2f19..1a715dab392 100644
--- a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.pl.xlf
+++ b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.pl.xlf
@@ -14,7 +14,7 @@
- Ślady {0}
+ Ślad {0}{0} is an application name
diff --git a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.pt-BR.xlf b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.pt-BR.xlf
index cae63e20e72..eb6bc712457 100644
--- a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.pt-BR.xlf
+++ b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.pt-BR.xlf
@@ -14,7 +14,7 @@
- {0} Rastreamentos
+ Rastreamento de{0}{0} is an application name
diff --git a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.ru.xlf b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.ru.xlf
index 383eb0433a1..1dcfa02739b 100644
--- a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.ru.xlf
+++ b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.ru.xlf
@@ -14,7 +14,7 @@
- Трассировок: {0}
+ Трассировка {0}{0} is an application name
diff --git a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.tr.xlf b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.tr.xlf
index bbc9a76afda..dee61498427 100644
--- a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.tr.xlf
+++ b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.tr.xlf
@@ -14,7 +14,7 @@
- {0} İzlemeleri
+ {0} izleme{0} is an application name
diff --git a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.zh-Hans.xlf b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.zh-Hans.xlf
index 2e6105d11c5..0e582d85bdd 100644
--- a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.zh-Hans.xlf
+++ b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.zh-Hans.xlf
@@ -14,7 +14,7 @@
- {0} 跟踪
+ {0} 跟踪{0} is an application name
diff --git a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.zh-Hant.xlf b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.zh-Hant.xlf
index 7ca33bbde5d..5ff3e196b9d 100644
--- a/src/Aspire.Dashboard/Resources/xlf/TraceDetail.zh-Hant.xlf
+++ b/src/Aspire.Dashboard/Resources/xlf/TraceDetail.zh-Hant.xlf
@@ -14,7 +14,7 @@
- {0} 追蹤
+ {0} 追蹤{0} is an application name
diff --git a/src/Aspire.Hosting.AWS/README.md b/src/Aspire.Hosting.AWS/README.md
index 3f85be76ffe..cbc253866a4 100644
--- a/src/Aspire.Hosting.AWS/README.md
+++ b/src/Aspire.Hosting.AWS/README.md
@@ -38,7 +38,7 @@ builder.AddProject("Frontend")
## Provisioning application resources with AWS CloudFormation
-AWS application resources like Amazon DynamoDB tables or Amazon Simple Queue Service (SQS) queues can be provisioning during AppHost
+AWS application resources like Amazon DynamoDB tables or Amazon Simple Queue Service (SQS) queues can be provisioned during AppHost
startup using a CloudFormation template.
In the AppHost project create either a JSON or YAML CloudFormation template. Here is an example template called `app-resources.template` that creates a queue and topic.
@@ -62,7 +62,7 @@ In the AppHost project create either a JSON or YAML CloudFormation template. Her
"Type" : "AWS::SNS::Topic",
"Properties" : {
"Subscription" : [
- {"Protocol" : "sqs", "Endpoint" : {"Fn::GetAtt" : [ "ChatMessagesQueue", "Arn"]}}
+ { "Protocol" : "sqs", "Endpoint" : { "Fn::GetAtt" : [ "ChatMessagesQueue", "Arn" ] } }
]
}
}
diff --git a/src/Aspire.Hosting.Azure.AppConfiguration/AzureAppConfigurationExtensions.cs b/src/Aspire.Hosting.Azure.AppConfiguration/AzureAppConfigurationExtensions.cs
index 3eb685352f4..2a7c306e567 100644
--- a/src/Aspire.Hosting.Azure.AppConfiguration/AzureAppConfigurationExtensions.cs
+++ b/src/Aspire.Hosting.Azure.AppConfiguration/AzureAppConfigurationExtensions.cs
@@ -37,6 +37,8 @@ public static IResourceBuilder AddAzureAppConfigu
[Experimental("ASPIRE0001", UrlFormat = "https://aka.ms/dotnet/aspire/diagnostics#{0}")]
public static IResourceBuilder AddAzureAppConfiguration(this IDistributedApplicationBuilder builder, string name, Action, ResourceModuleConstruct, AppConfigurationStore>? configureResource)
{
+ builder.AddAzureProvisioning();
+
var configureConstruct = (ResourceModuleConstruct construct) =>
{
var store = new AppConfigurationStore(construct, name: name, skuName: "standard");
diff --git a/src/Aspire.Hosting.Azure.ApplicationInsights/Aspire.Hosting.Azure.ApplicationInsights.csproj b/src/Aspire.Hosting.Azure.ApplicationInsights/Aspire.Hosting.Azure.ApplicationInsights.csproj
index af645d7ba6e..512f1dd1259 100644
--- a/src/Aspire.Hosting.Azure.ApplicationInsights/Aspire.Hosting.Azure.ApplicationInsights.csproj
+++ b/src/Aspire.Hosting.Azure.ApplicationInsights/Aspire.Hosting.Azure.ApplicationInsights.csproj
@@ -9,6 +9,7 @@
+
diff --git a/src/Aspire.Hosting.Azure.ApplicationInsights/AzureApplicationInsightsExtensions.cs b/src/Aspire.Hosting.Azure.ApplicationInsights/AzureApplicationInsightsExtensions.cs
index 34000251819..60125641bed 100644
--- a/src/Aspire.Hosting.Azure.ApplicationInsights/AzureApplicationInsightsExtensions.cs
+++ b/src/Aspire.Hosting.Azure.ApplicationInsights/AzureApplicationInsightsExtensions.cs
@@ -23,7 +23,21 @@ public static class AzureApplicationInsightsExtensions
public static IResourceBuilder AddAzureApplicationInsights(this IDistributedApplicationBuilder builder, string name)
{
#pragma warning disable ASPIRE0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
- return builder.AddAzureApplicationInsights(name, (_, _, _) => { });
+ return builder.AddAzureApplicationInsights(name, null, null);
+#pragma warning restore ASPIRE0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
+ }
+
+ ///
+ /// Adds an Azure Application Insights resource to the application model.
+ ///
+ /// The .
+ /// The name of the resource. This name will be used as the connection string name when referenced in a dependency.
+ /// A resource builder for the log analytics workspace.
+ /// A reference to the .
+ public static IResourceBuilder AddAzureApplicationInsights(this IDistributedApplicationBuilder builder, string name, IResourceBuilder? logAnalyticsWorkspace)
+ {
+#pragma warning disable ASPIRE0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
+ return builder.AddAzureApplicationInsights(name, logAnalyticsWorkspace, null);
#pragma warning restore ASPIRE0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
}
@@ -37,13 +51,41 @@ public static IResourceBuilder AddAzureApplica
[Experimental("ASPIRE0001", UrlFormat = "https://aka.ms/dotnet/aspire/diagnostics#{0}")]
public static IResourceBuilder AddAzureApplicationInsights(this IDistributedApplicationBuilder builder, string name, Action, ResourceModuleConstruct, ApplicationInsightsComponent>? configureResource)
{
+ return builder.AddAzureApplicationInsights(name, null, configureResource);
+ }
+
+ ///
+ /// Adds an Azure Application Insights resource to the application model.
+ ///
+ /// The builder for the distributed application.
+ /// The name of the resource.
+ /// A resource builder for the log analytics workspace.
+ /// Optional callback to configure the Application Insights resource.
+ ///
+ [Experimental("ASPIRE0001", UrlFormat = "https://aka.ms/dotnet/aspire/diagnostics#{0}")]
+ public static IResourceBuilder AddAzureApplicationInsights(this IDistributedApplicationBuilder builder, string name, IResourceBuilder? logAnalyticsWorkspace, Action, ResourceModuleConstruct, ApplicationInsightsComponent>? configureResource)
+ {
+ builder.AddAzureProvisioning();
+
var configureConstruct = (ResourceModuleConstruct construct) =>
{
var appInsights = new ApplicationInsightsComponent(construct, name: name);
appInsights.Properties.Tags["aspire-resource-name"] = construct.Resource.Name;
appInsights.AssignProperty(p => p.ApplicationType, new Parameter("applicationType", defaultValue: "web"));
appInsights.AssignProperty(p => p.Kind, new Parameter("kind", defaultValue: "web"));
- appInsights.AssignProperty(p => p.WorkspaceResourceId, new Parameter(AzureBicepResource.KnownParameters.LogAnalyticsWorkspaceId));
+
+ if (logAnalyticsWorkspace != null)
+ {
+ appInsights.AssignProperty(p => p.WorkspaceResourceId, logAnalyticsWorkspace.Resource.WorkspaceId, AzureBicepResource.KnownParameters.LogAnalyticsWorkspaceId);
+ }
+ else
+ {
+ // If the user does not supply a log analytics workspace of their own we still create a parameter on the Aspire
+ // side and the CDK side so that AZD can fill the value in with the one it generates.
+ construct.Resource.Parameters.Add(AzureBicepResource.KnownParameters.LogAnalyticsWorkspaceId, "");
+ appInsights.AssignProperty(p => p.WorkspaceResourceId, new Parameter(AzureBicepResource.KnownParameters.LogAnalyticsWorkspaceId));
+
+ }
appInsights.AddOutput("appInsightsConnectionString", p => p.ConnectionString);
@@ -54,6 +96,7 @@ public static IResourceBuilder AddAzureApplica
configureResource(resourceBuilder, construct, appInsights);
}
};
+
var resource = new AzureApplicationInsightsResource(name, configureConstruct);
return builder.AddResource(resource)
diff --git a/src/Aspire.Hosting.Azure.CognitiveServices/AzureOpenAIExtensions.cs b/src/Aspire.Hosting.Azure.CognitiveServices/AzureOpenAIExtensions.cs
index 74ec39035aa..79a3e462507 100644
--- a/src/Aspire.Hosting.Azure.CognitiveServices/AzureOpenAIExtensions.cs
+++ b/src/Aspire.Hosting.Azure.CognitiveServices/AzureOpenAIExtensions.cs
@@ -38,6 +38,8 @@ public static IResourceBuilder AddAzureOpenAI(this IDistrib
[Experimental("ASPIRE0001", UrlFormat = "https://aka.ms/dotnet/aspire/diagnostics#{0}")]
public static IResourceBuilder AddAzureOpenAI(this IDistributedApplicationBuilder builder, string name, Action, ResourceModuleConstruct, CognitiveServicesAccount, IEnumerable>? configureResource)
{
+ builder.AddAzureProvisioning();
+
var configureConstruct = (ResourceModuleConstruct construct) =>
{
var cogServicesAccount = new CognitiveServicesAccount(construct, "OpenAI", name: name);
diff --git a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs
index c4ae79baf0d..3f558fb723d 100644
--- a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs
+++ b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs
@@ -38,6 +38,8 @@ public static IResourceBuilder AddAzureCosmosDB(this IDis
[Experimental("ASPIRE0001", UrlFormat = "https://aka.ms/dotnet/aspire/diagnostics#{0}")]
public static IResourceBuilder AddAzureCosmosDB(this IDistributedApplicationBuilder builder, string name, Action, ResourceModuleConstruct, CosmosDBAccount, IEnumerable>? configureResource)
{
+ builder.AddAzureProvisioning();
+
var configureConstruct = (ResourceModuleConstruct construct) =>
{
var cosmosAccount = new CosmosDBAccount(construct, CosmosDBAccountKind.GlobalDocumentDB, name: name);
@@ -61,7 +63,7 @@ public static IResourceBuilder AddAzureCosmosDB(this IDis
}
var keyVault = KeyVault.FromExisting(construct, "keyVaultName");
- _ = new KeyVaultSecret(construct, "connectionString", cosmosAccount.GetConnectionString());
+ _ = new KeyVaultSecret(construct, "connectionString", cosmosAccount.GetConnectionString(), keyVault);
configureResource?.Invoke(azureResourceBuilder, construct, cosmosAccount, cosmosSqlDatabases);
};
@@ -90,7 +92,7 @@ public static IResourceBuilder RunAsEmulator(this IResour
return builder;
}
- builder.WithEndpoint(name: "emulator", containerPort: 8081)
+ builder.WithEndpoint(name: "emulator", targetPort: 8081)
.WithAnnotation(new ContainerImageAnnotation { Image = "mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator", Tag = "latest" });
if (configureContainer != null)
diff --git a/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsExtensions.cs b/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsExtensions.cs
index d839ddcb6df..2fb002d011d 100644
--- a/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsExtensions.cs
+++ b/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsExtensions.cs
@@ -40,6 +40,8 @@ public static IResourceBuilder AddAzureEventHubs(
public static IResourceBuilder AddAzureEventHubs(this IDistributedApplicationBuilder builder, string name,
Action, ResourceModuleConstruct, EventHubsNamespace>? configureResource)
{
+ builder.AddAzureProvisioning();
+
var configureConstruct = (ResourceModuleConstruct construct) =>
{
var eventHubsNamespace = new EventHubsNamespace(construct, name: name);
diff --git a/src/Aspire.Hosting.Azure.KeyVault/AzureKeyVaultResourceExtensions.cs b/src/Aspire.Hosting.Azure.KeyVault/AzureKeyVaultResourceExtensions.cs
index 7c65832e452..babad98699a 100644
--- a/src/Aspire.Hosting.Azure.KeyVault/AzureKeyVaultResourceExtensions.cs
+++ b/src/Aspire.Hosting.Azure.KeyVault/AzureKeyVaultResourceExtensions.cs
@@ -37,6 +37,8 @@ public static IResourceBuilder AddAzureKeyVault(this IDis
[Experimental("ASPIRE0001", UrlFormat = "https://aka.ms/dotnet/aspire/diagnostics#{0}")]
public static IResourceBuilder AddAzureKeyVault(this IDistributedApplicationBuilder builder, string name, Action, ResourceModuleConstruct, KeyVault>? configureResource)
{
+ builder.AddAzureProvisioning();
+
var configureConstruct = (ResourceModuleConstruct construct) =>
{
var keyVault = construct.AddKeyVault(name: construct.Resource.Name);
diff --git a/src/Aspire.Hosting.Azure.OperationalInsights/AzureLogAnalyticsWorkspaceExtensions.cs b/src/Aspire.Hosting.Azure.OperationalInsights/AzureLogAnalyticsWorkspaceExtensions.cs
index 8343455edf7..852b466200f 100644
--- a/src/Aspire.Hosting.Azure.OperationalInsights/AzureLogAnalyticsWorkspaceExtensions.cs
+++ b/src/Aspire.Hosting.Azure.OperationalInsights/AzureLogAnalyticsWorkspaceExtensions.cs
@@ -37,6 +37,8 @@ public static IResourceBuilder AddAzureLogAn
[Experimental("ASPIRE0001", UrlFormat = "https://aka.ms/dotnet/aspire/diagnostics#{0}")]
public static IResourceBuilder AddAzureLogAnalyticsWorkspace(this IDistributedApplicationBuilder builder, string name, Action, ResourceModuleConstruct, OperationalInsightsWorkspace>? configureResource)
{
+ builder.AddAzureProvisioning();
+
var configureConstruct = (ResourceModuleConstruct construct) =>
{
var workspace = new OperationalInsightsWorkspace(construct, name: name, sku: new OperationalInsightsWorkspaceSku(OperationalInsightsWorkspaceSkuName.PerGB2018));
diff --git a/src/Aspire.Hosting.Azure.PostgreSQL/AzurePostgresExtensions.cs b/src/Aspire.Hosting.Azure.PostgreSQL/AzurePostgresExtensions.cs
index d9f98087c44..00d3ccf8c6a 100644
--- a/src/Aspire.Hosting.Azure.PostgreSQL/AzurePostgresExtensions.cs
+++ b/src/Aspire.Hosting.Azure.PostgreSQL/AzurePostgresExtensions.cs
@@ -47,17 +47,18 @@ internal static IResourceBuilder PublishAsAzurePostgresF
Action, ResourceModuleConstruct, PostgreSqlFlexibleServer>? configureResource,
bool useProvisioner = false)
{
+ builder.ApplicationBuilder.AddAzureProvisioning();
+
var configureConstruct = (ResourceModuleConstruct construct) =>
{
var administratorLogin = new Parameter("administratorLogin");
var administratorLoginPassword = new Parameter("administratorLoginPassword", isSecure: true);
- var postgres = new PostgreSqlFlexibleServer(construct, administratorLogin, administratorLoginPassword, name: construct.Resource.Name);
+ var postgres = new PostgreSqlFlexibleServer(construct, administratorLogin, administratorLoginPassword, name: construct.Resource.Name, storageSizeInGB: 32);
postgres.AssignProperty(x => x.Sku.Name, "'Standard_B1ms'");
postgres.AssignProperty(x => x.Sku.Tier, "'Burstable'");
postgres.AssignProperty(x => x.Version, "'16'");
postgres.AssignProperty(x => x.HighAvailability.Mode, "'Disabled'");
- postgres.AssignProperty(x => x.Storage.StorageSizeInGB, "32");
postgres.AssignProperty(x => x.Backup.BackupRetentionDays, "7");
postgres.AssignProperty(x => x.Backup.GeoRedundantBackup, "'Disabled'");
postgres.AssignProperty(x => x.AvailabilityZone, "'1'");
@@ -82,7 +83,7 @@ internal static IResourceBuilder PublishAsAzurePostgresF
}
var keyVault = KeyVault.FromExisting(construct, "keyVaultName");
- _ = new KeyVaultSecret(construct, "connectionString", postgres.GetConnectionString(administratorLogin, administratorLoginPassword));
+ _ = new KeyVaultSecret(construct, "connectionString", postgres.GetConnectionString(administratorLogin, administratorLoginPassword), keyVault);
var azureResource = (AzurePostgresResource)construct.Resource;
var azureResourceBuilder = builder.ApplicationBuilder.CreateResourceBuilder(azureResource);
diff --git a/src/Aspire.Hosting.Azure.Redis/AzureRedisExtensions.cs b/src/Aspire.Hosting.Azure.Redis/AzureRedisExtensions.cs
index 51d834661e5..24839384574 100644
--- a/src/Aspire.Hosting.Azure.Redis/AzureRedisExtensions.cs
+++ b/src/Aspire.Hosting.Azure.Redis/AzureRedisExtensions.cs
@@ -41,6 +41,8 @@ public static IResourceBuilder PublishAsAzureRedis(this IResource
internal static IResourceBuilder PublishAsAzureRedisInternal(this IResourceBuilder builder, Action, ResourceModuleConstruct, RedisCache>? configureResource, bool useProvisioner = false)
{
+ builder.ApplicationBuilder.AddAzureProvisioning();
+
var configureConstruct = (ResourceModuleConstruct construct) =>
{
var redisCache = new RedisCache(construct, name: builder.Resource.Name);
diff --git a/src/Aspire.Hosting.Azure.Search/AzureSearchExtensions.cs b/src/Aspire.Hosting.Azure.Search/AzureSearchExtensions.cs
index 79a529c028f..6eb0963c6d8 100644
--- a/src/Aspire.Hosting.Azure.Search/AzureSearchExtensions.cs
+++ b/src/Aspire.Hosting.Azure.Search/AzureSearchExtensions.cs
@@ -40,6 +40,8 @@ public static IResourceBuilder AddAzureSearch(
string name,
Action, ResourceModuleConstruct, SearchService>? configureResource)
{
+ builder.AddAzureProvisioning();
+
AzureSearchResource resource = new(name, ConfigureSearch);
return builder.AddResource(resource)
.WithParameter(AzureBicepResource.KnownParameters.PrincipalId)
diff --git a/src/Aspire.Hosting.Azure.ServiceBus/AzureServiceBusExtensions.cs b/src/Aspire.Hosting.Azure.ServiceBus/AzureServiceBusExtensions.cs
index 499a7886ecb..3a11a518e47 100644
--- a/src/Aspire.Hosting.Azure.ServiceBus/AzureServiceBusExtensions.cs
+++ b/src/Aspire.Hosting.Azure.ServiceBus/AzureServiceBusExtensions.cs
@@ -38,6 +38,8 @@ public static IResourceBuilder AddAzureServiceBus(this
[Experimental("ASPIRE0001", UrlFormat = "https://aka.ms/dotnet/aspire/diagnostics#{0}")]
public static IResourceBuilder AddAzureServiceBus(this IDistributedApplicationBuilder builder, string name, Action, ResourceModuleConstruct, ServiceBusNamespace>? configureResource)
{
+ builder.AddAzureProvisioning();
+
var configureConstruct = (ResourceModuleConstruct construct) =>
{
var serviceBusNamespace = new ServiceBusNamespace(construct, name: name);
diff --git a/src/Aspire.Hosting.Azure.SignalR/AzureSignalRExtensions.cs b/src/Aspire.Hosting.Azure.SignalR/AzureSignalRExtensions.cs
index e4c4325c99a..b95de833d68 100644
--- a/src/Aspire.Hosting.Azure.SignalR/AzureSignalRExtensions.cs
+++ b/src/Aspire.Hosting.Azure.SignalR/AzureSignalRExtensions.cs
@@ -37,6 +37,8 @@ public static IResourceBuilder AddAzureSignalR(this IDistr
[Experimental("ASPIRE0001", UrlFormat = "https://aka.ms/dotnet/aspire/diagnostics#{0}")]
public static IResourceBuilder AddAzureSignalR(this IDistributedApplicationBuilder builder, string name, Action, ResourceModuleConstruct, SignalRService>? configureResource)
{
+ builder.AddAzureProvisioning();
+
var configureConstruct = (ResourceModuleConstruct construct) =>
{
var service = new SignalRService(construct, name: name);
diff --git a/src/Aspire.Hosting.Azure.Sql/AzureSqlExtensions.cs b/src/Aspire.Hosting.Azure.Sql/AzureSqlExtensions.cs
index 2fc59051715..c08e6401442 100644
--- a/src/Aspire.Hosting.Azure.Sql/AzureSqlExtensions.cs
+++ b/src/Aspire.Hosting.Azure.Sql/AzureSqlExtensions.cs
@@ -15,6 +15,8 @@ public static class AzureSqlExtensions
{
internal static IResourceBuilder PublishAsAzureSqlDatabase(this IResourceBuilder builder, Action, ResourceModuleConstruct, SqlServer, IEnumerable>? configureResource, bool useProvisioner = false)
{
+ builder.ApplicationBuilder.AddAzureProvisioning();
+
var configureConstruct = (ResourceModuleConstruct construct) =>
{
var sqlServer = new SqlServer(construct, builder.Resource.Name);
diff --git a/src/Aspire.Hosting.Azure.Storage/AzureStorageExtensions.cs b/src/Aspire.Hosting.Azure.Storage/AzureStorageExtensions.cs
index 7c4486f405c..c7220c8a703 100644
--- a/src/Aspire.Hosting.Azure.Storage/AzureStorageExtensions.cs
+++ b/src/Aspire.Hosting.Azure.Storage/AzureStorageExtensions.cs
@@ -39,6 +39,8 @@ public static IResourceBuilder AddAzureStorage(this IDistr
[Experimental("ASPIRE0001", UrlFormat = "https://aka.ms/dotnet/aspire/diagnostics#{0}")]
public static IResourceBuilder AddAzureStorage(this IDistributedApplicationBuilder builder, string name, Action, ResourceModuleConstruct, StorageAccount>? configureResource)
{
+ builder.AddAzureProvisioning();
+
var configureConstruct = (ResourceModuleConstruct construct) =>
{
var storageAccount = construct.AddStorageAccount(
@@ -93,9 +95,9 @@ public static IResourceBuilder RunAsEmulator(this IResourc
return builder;
}
- builder.WithEndpoint(name: "blob", containerPort: 10000)
- .WithEndpoint(name: "queue", containerPort: 10001)
- .WithEndpoint(name: "table", containerPort: 10002)
+ builder.WithEndpoint(name: "blob", targetPort: 10000)
+ .WithEndpoint(name: "queue", targetPort: 10001)
+ .WithEndpoint(name: "table", targetPort: 10002)
.WithAnnotation(new ContainerImageAnnotation { Image = "mcr.microsoft.com/azure-storage/azurite", Tag = "3.29.0" });
if (configureContainer != null)
@@ -108,30 +110,6 @@ public static IResourceBuilder RunAsEmulator(this IResourc
return builder;
}
- ///
- /// Configures an Azure Storage resource to be emulated using Azurite. This resource requires an to be added to the application model. This version the package defaults to version 3.29.0 of the mcr.microsoft.com/azure-storage/azurite container image.
- ///
- /// The Azure storage resource builder.
- /// Callback that exposes underlying container used for emulation to allow for customization.
- /// A reference to the .
- [Obsolete("Renamed to RunAsEmulator. Will be removed in next preview.")]
- public static IResourceBuilder UseEmulator(this IResourceBuilder builder, Action>? configureContainer = null)
- {
- return builder.RunAsEmulator(configureContainer);
- }
-
- ///
- /// Enables persistence in the Azure Storage emulator.
- ///
- /// The builder for the .
- /// Relative path to the AppHost where emulator storage is persisted between runs.
- /// A builder for the .
- [Obsolete("Use WithDataBindMount or WithDataVolume instead. Will be removed in next preview.")]
- public static IResourceBuilder UsePersistence(this IResourceBuilder builder, string? path = null)
- {
- return builder.WithDataBindMount(path);
- }
-
///
/// Adds a bind mount for the data folder to an Azure Storage emulator resource.
///
diff --git a/src/Aspire.Hosting.Azure/Aspire.Hosting.Azure.csproj b/src/Aspire.Hosting.Azure/Aspire.Hosting.Azure.csproj
index 562ad185d0a..7a4da38ea08 100644
--- a/src/Aspire.Hosting.Azure/Aspire.Hosting.Azure.csproj
+++ b/src/Aspire.Hosting.Azure/Aspire.Hosting.Azure.csproj
@@ -24,10 +24,7 @@
-
-
-
diff --git a/src/Aspire.Hosting.Azure/AzureBicepResource.cs b/src/Aspire.Hosting.Azure/AzureBicepResource.cs
index e3d41d3fcff..73fd0496fa5 100644
--- a/src/Aspire.Hosting.Azure/AzureBicepResource.cs
+++ b/src/Aspire.Hosting.Azure/AzureBicepResource.cs
@@ -74,7 +74,7 @@ public virtual BicepTemplateFile GetBicepTemplateFile(string? directory = null,
isTempFile = directory is null;
path = TempDirectory is null
- ? Path.GetTempFileName() + ".bicep"
+ ? Path.Combine(directory ?? Directory.CreateTempSubdirectory("aspire").FullName, $"{Name.ToLowerInvariant()}.bicep")
: Path.Combine(TempDirectory, $"{Name.ToLowerInvariant()}.bicep");
if (TemplateResourceName is null)
@@ -127,14 +127,6 @@ public virtual string GetBicepTemplateString()
return File.ReadAllText(TemplateFile);
}
- /// TODO: Remove this method once AppInsights CDK is removed.
- ///
- /// Create a Bicep identifier safe version of the resource name.
- ///
- /// A string which is safe to use as a Bicep identifier.
- [Obsolete("This method is obsolete and will be removed before release.")]
- public string CreateBicepResourceName() => Name.ToLower(); // Insufficient but we don't care because its going to be deleted.
-
///
/// Writes the resource to the manifest.
///
diff --git a/src/Aspire.Hosting.Azure/AzureBicepResourceExtensions.cs b/src/Aspire.Hosting.Azure/AzureBicepResourceExtensions.cs
index 36239b8bdc4..9d6927fd724 100644
--- a/src/Aspire.Hosting.Azure/AzureBicepResourceExtensions.cs
+++ b/src/Aspire.Hosting.Azure/AzureBicepResourceExtensions.cs
@@ -22,6 +22,8 @@ public static class AzureBicepResourceExtensions
/// An .
public static IResourceBuilder AddBicepTemplate(this IDistributedApplicationBuilder builder, string name, string bicepFile)
{
+ builder.AddAzureProvisioning();
+
var path = Path.GetFullPath(Path.Combine(builder.AppHostDirectory, bicepFile));
var resource = new AzureBicepResource(name, templateFile: path, templateString: null);
return builder.AddResource(resource)
@@ -37,6 +39,8 @@ public static IResourceBuilder AddBicepTemplate(this IDistri
/// An .
public static IResourceBuilder AddBicepTemplateString(this IDistributedApplicationBuilder builder, string name, string bicepContent)
{
+ builder.AddAzureProvisioning();
+
var resource = new AzureBicepResource(name, templateFile: null, templateString: bicepContent);
return builder.AddResource(resource)
.WithManifestPublishingCallback(resource.WriteToManifest);
diff --git a/src/Aspire.Hosting.Azure/AzureConstructResource.cs b/src/Aspire.Hosting.Azure/AzureConstructResource.cs
index 48a524ebf29..081c572d035 100644
--- a/src/Aspire.Hosting.Azure/AzureConstructResource.cs
+++ b/src/Aspire.Hosting.Azure/AzureConstructResource.cs
@@ -92,6 +92,8 @@ public static class AzureConstructResourceExtensions
///
public static IResourceBuilder AddAzureConstruct(this IDistributedApplicationBuilder builder, string name, Action configureConstruct)
{
+ builder.AddAzureProvisioning();
+
var resource = new AzureConstructResource(name, configureConstruct);
return builder.AddResource(resource)
.WithManifestPublishingCallback(resource.WriteToManifest);
diff --git a/src/Aspire.Hosting.Azure/Exceptions.cs b/src/Aspire.Hosting.Azure/Exceptions.cs
new file mode 100644
index 00000000000..b6518fbbb3a
--- /dev/null
+++ b/src/Aspire.Hosting.Azure/Exceptions.cs
@@ -0,0 +1,19 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Aspire.Hosting.Azure;
+
+internal class AzureCliNotOnPathException : DistributedApplicationException
+{
+ public AzureCliNotOnPathException() { }
+ public AzureCliNotOnPathException(string message) : base(message) { }
+ public AzureCliNotOnPathException(string message, Exception inner) : base(message, inner) { }
+}
+
+internal class FailedToApplyEnvironmentException : DistributedApplicationException
+{
+ public FailedToApplyEnvironmentException() { }
+ public FailedToApplyEnvironmentException(string message) : base(message) { }
+ public FailedToApplyEnvironmentException(string message, Exception inner) : base(message, inner) { }
+}
+
diff --git a/src/Aspire.Hosting.Azure/Provisioning/Provisioners/AzureProvisioner.cs b/src/Aspire.Hosting.Azure/Provisioning/Provisioners/AzureProvisioner.cs
index 352fa3c3a54..15f8197ed2b 100644
--- a/src/Aspire.Hosting.Azure/Provisioning/Provisioners/AzureProvisioner.cs
+++ b/src/Aspire.Hosting.Azure/Provisioning/Provisioners/AzureProvisioner.cs
@@ -104,6 +104,14 @@ await UpdateStateAsync(resource, s => s with
})
.ConfigureAwait(false);
}
+ catch (MissingConfigurationException)
+ {
+ await UpdateStateAsync(resource, s => s with
+ {
+ State = new("Missing subscription configuration", KnownResourceStateStyles.Error)
+ })
+ .ConfigureAwait(false);
+ }
catch (Exception)
{
await UpdateStateAsync(resource, s => s with
@@ -263,6 +271,16 @@ await provisioner.GetOrCreateResourceAsync(
resource.ProvisioningTaskCompletionSource?.TrySetResult();
}
+ catch (AzureCliNotOnPathException ex)
+ {
+ resourceLogger.LogCritical("Using Azure resources during local development requires the installation of the Azure CLI. See https://aka.ms/dotnet/aspire/azcli for instructions.");
+ resource.ProvisioningTaskCompletionSource?.TrySetException(ex);
+ }
+ catch (MissingConfigurationException ex)
+ {
+ resourceLogger.LogCritical("Resource could not be provisioned because Azure subscription, location, and resource group information is missing. See https://aka.ms/dotnet/aspire/azure/provisioning for more details.");
+ resource.ProvisioningTaskCompletionSource?.TrySetException(ex);
+ }
catch (JsonException ex)
{
resourceLogger.LogError(ex, "Error provisioning {ResourceName} because user secrets file is not well-formed JSON.", resource.Name);
diff --git a/src/Aspire.Hosting.Azure/Provisioning/Provisioners/BicepProvisioner.cs b/src/Aspire.Hosting.Azure/Provisioning/Provisioners/BicepProvisioner.cs
index 4b5afb5d359..30bf2cfc608 100644
--- a/src/Aspire.Hosting.Azure/Provisioning/Provisioners/BicepProvisioner.cs
+++ b/src/Aspire.Hosting.Azure/Provisioning/Provisioners/BicepProvisioner.cs
@@ -123,8 +123,10 @@ await notificationService.PublishUpdateAsync(resource, state => state with
PopulateWellKnownParameters(resource, context);
- var azPath = FindFullPathFromPath("az") ??
- throw new InvalidOperationException("Azure CLI not found in PATH");
+ if (FindFullPathFromPath("az") is not { } azPath)
+ {
+ throw new AzureCliNotOnPathException();
+ }
var template = resource.GetBicepTemplateFile();
diff --git a/src/Aspire.Hosting.Dapr/Aspire.Hosting.Dapr.csproj b/src/Aspire.Hosting.Dapr/Aspire.Hosting.Dapr.csproj
index fa4bb208b5d..2f0b52fd6f9 100644
--- a/src/Aspire.Hosting.Dapr/Aspire.Hosting.Dapr.csproj
+++ b/src/Aspire.Hosting.Dapr/Aspire.Hosting.Dapr.csproj
@@ -15,4 +15,8 @@
+
+
+
+
diff --git a/src/Aspire.Hosting.Dapr/DaprDistributedApplicationLifecycleHook.cs b/src/Aspire.Hosting.Dapr/DaprDistributedApplicationLifecycleHook.cs
index 3ee7e1a0f56..373fde2962c 100644
--- a/src/Aspire.Hosting.Dapr/DaprDistributedApplicationLifecycleHook.cs
+++ b/src/Aspire.Hosting.Dapr/DaprDistributedApplicationLifecycleHook.cs
@@ -151,9 +151,9 @@ public async Task BeforeStartAsync(DistributedApplicationModel appModel, Cancell
context.EnvironmentVariables.TryAdd("DAPR_HTTP_ENDPOINT", http);
}));
- daprCli.Annotations.Add(new EndpointAnnotation(ProtocolType.Tcp, name: "grpc", port: sidecarOptions?.DaprGrpcPort));
- daprCli.Annotations.Add(new EndpointAnnotation(ProtocolType.Tcp, name: "http", port: sidecarOptions?.DaprHttpPort));
- daprCli.Annotations.Add(new EndpointAnnotation(ProtocolType.Tcp, name: "metrics", port: sidecarOptions?.MetricsPort));
+ daprCli.Annotations.Add(new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "http", name: "grpc", port: sidecarOptions?.DaprGrpcPort));
+ daprCli.Annotations.Add(new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "http", name: "http", port: sidecarOptions?.DaprHttpPort));
+ daprCli.Annotations.Add(new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "http", name: "metrics", port: sidecarOptions?.MetricsPort));
if (sidecarOptions?.EnableProfiling == true)
{
daprCli.Annotations.Add(new EndpointAnnotation(ProtocolType.Tcp, name: "profile", port: sidecarOptions?.ProfilePort));
diff --git a/src/Aspire.Hosting.Dapr/IDistributedApplicationBuilderExtensions.cs b/src/Aspire.Hosting.Dapr/IDistributedApplicationBuilderExtensions.cs
index 424c7a205af..7ee47ca3da7 100644
--- a/src/Aspire.Hosting.Dapr/IDistributedApplicationBuilderExtensions.cs
+++ b/src/Aspire.Hosting.Dapr/IDistributedApplicationBuilderExtensions.cs
@@ -46,6 +46,12 @@ public static IResourceBuilder AddDaprComponent(this IDi
return builder
.AddResource(resource)
+ .WithInitialState(new()
+ {
+ Properties = [],
+ ResourceType = "DaprComponent",
+ State = "Hidden"
+ })
.WithAnnotation(new ManifestPublishingCallbackAnnotation(context => WriteDaprComponentResourceToManifest(context, resource)));
}
diff --git a/src/Aspire.Hosting.Dapr/IDistributedApplicationComponentBuilderExtensions.cs b/src/Aspire.Hosting.Dapr/IDistributedApplicationComponentBuilderExtensions.cs
index 9f5c5819cce..0426de300e8 100644
--- a/src/Aspire.Hosting.Dapr/IDistributedApplicationComponentBuilderExtensions.cs
+++ b/src/Aspire.Hosting.Dapr/IDistributedApplicationComponentBuilderExtensions.cs
@@ -54,7 +54,13 @@ public static IResourceBuilder WithDaprSidecar(this IResourceBuilder bu
// Add Dapr is idempoent, so we can call it multiple times.
builder.ApplicationBuilder.AddDapr();
- var sidecarBuilder = builder.ApplicationBuilder.AddResource(new DaprSidecarResource($"{builder.Resource.Name}-dapr"));
+ var sidecarBuilder = builder.ApplicationBuilder.AddResource(new DaprSidecarResource($"{builder.Resource.Name}-dapr"))
+ .WithInitialState(new()
+ {
+ Properties = [],
+ ResourceType = "DaprSidecar",
+ State = "Hidden"
+ });
configureSidecar(sidecarBuilder);
diff --git a/src/Aspire.Hosting.Kafka/KafkaBuilderExtensions.cs b/src/Aspire.Hosting.Kafka/KafkaBuilderExtensions.cs
index bb4de60ce8f..c4870fd05e1 100644
--- a/src/Aspire.Hosting.Kafka/KafkaBuilderExtensions.cs
+++ b/src/Aspire.Hosting.Kafka/KafkaBuilderExtensions.cs
@@ -24,7 +24,7 @@ public static IResourceBuilder AddKafka(this IDistributedAp
{
var kafka = new KafkaServerResource(name);
return builder.AddResource(kafka)
- .WithEndpoint(containerPort: KafkaBrokerPort, hostPort: port, name: KafkaServerResource.PrimaryEndpointName)
+ .WithEndpoint(targetPort: KafkaBrokerPort, port: port, name: KafkaServerResource.PrimaryEndpointName)
.WithImage("confluentinc/confluent-local", "7.6.0")
.WithEnvironment(context => ConfigureKafkaContainer(context, kafka));
}
diff --git a/src/Aspire.Hosting.MongoDB/MongoDBBuilderExtensions.cs b/src/Aspire.Hosting.MongoDB/MongoDBBuilderExtensions.cs
index 06536209c70..61c9e940f45 100644
--- a/src/Aspire.Hosting.MongoDB/MongoDBBuilderExtensions.cs
+++ b/src/Aspire.Hosting.MongoDB/MongoDBBuilderExtensions.cs
@@ -28,7 +28,7 @@ public static IResourceBuilder AddMongoDB(this IDistribut
return builder
.AddResource(mongoDBContainer)
- .WithEndpoint(hostPort: port, containerPort: DefaultContainerPort, name: MongoDBServerResource.PrimaryEndpointName)
+ .WithEndpoint(port: port, targetPort: DefaultContainerPort, name: MongoDBServerResource.PrimaryEndpointName)
.WithImage(MongoDBContainerImageTags.Image, MongoDBContainerImageTags.Tag);
}
@@ -66,7 +66,7 @@ public static IResourceBuilder WithMongoExpress(this IResourceBuilder b
builder.ApplicationBuilder.AddResource(mongoExpressContainer)
.WithImage("mongo-express", "1.0.2-20")
.WithEnvironment(context => ConfigureMongoExpressContainer(context, builder.Resource))
- .WithHttpEndpoint(containerPort: 8081, hostPort: hostPort, name: containerName)
+ .WithHttpEndpoint(targetPort: 8081, port: hostPort, name: containerName)
.ExcludeFromManifest();
return builder;
diff --git a/src/Aspire.Hosting.MySql/MySqlBuilderExtensions.cs b/src/Aspire.Hosting.MySql/MySqlBuilderExtensions.cs
index 2e68d436f56..a2d22349c97 100644
--- a/src/Aspire.Hosting.MySql/MySqlBuilderExtensions.cs
+++ b/src/Aspire.Hosting.MySql/MySqlBuilderExtensions.cs
@@ -29,7 +29,7 @@ public static IResourceBuilder AddMySql(this IDistributedAp
var resource = new MySqlServerResource(name, passwordParameter);
return builder.AddResource(resource)
- .WithEndpoint(hostPort: port, containerPort: 3306, name: MySqlServerResource.PrimaryEndpointName) // Internal port is always 3306.
+ .WithEndpoint(port: port, targetPort: 3306, name: MySqlServerResource.PrimaryEndpointName) // Internal port is always 3306.
.WithImage(MySqlContainerImageTags.Image, MySqlContainerImageTags.Tag)
.WithEnvironment(context =>
{
@@ -75,7 +75,7 @@ public static IResourceBuilder WithPhpMyAdmin(this IResourceBuilder bui
var phpMyAdminContainer = new PhpMyAdminContainerResource(containerName);
builder.ApplicationBuilder.AddResource(phpMyAdminContainer)
.WithImage("phpmyadmin", "5.2")
- .WithHttpEndpoint(containerPort: 80, hostPort: hostPort, name: containerName)
+ .WithHttpEndpoint(targetPort: 80, port: hostPort, name: containerName)
.WithBindMount(Path.GetTempFileName(), "/etc/phpmyadmin/config.user.inc.php")
.ExcludeFromManifest();
diff --git a/src/Aspire.Hosting.Nats/NatsBuilderExtensions.cs b/src/Aspire.Hosting.Nats/NatsBuilderExtensions.cs
index 8885fb4f1ad..1b28ef41786 100644
--- a/src/Aspire.Hosting.Nats/NatsBuilderExtensions.cs
+++ b/src/Aspire.Hosting.Nats/NatsBuilderExtensions.cs
@@ -22,7 +22,7 @@ public static IResourceBuilder AddNats(this IDistributedAppl
{
var nats = new NatsServerResource(name);
return builder.AddResource(nats)
- .WithEndpoint(containerPort: 4222, hostPort: port, name: NatsServerResource.PrimaryEndpointName)
+ .WithEndpoint(targetPort: 4222, port: port, name: NatsServerResource.PrimaryEndpointName)
.WithImage(NatsContainerImageTags.Image, NatsContainerImageTags.Tag);
}
diff --git a/src/Aspire.Hosting.Oracle/OracleDatabaseBuilderExtensions.cs b/src/Aspire.Hosting.Oracle/OracleDatabaseBuilderExtensions.cs
index 4a5c752ece3..dab8b84b45d 100644
--- a/src/Aspire.Hosting.Oracle/OracleDatabaseBuilderExtensions.cs
+++ b/src/Aspire.Hosting.Oracle/OracleDatabaseBuilderExtensions.cs
@@ -13,21 +13,6 @@ public static class OracleDatabaseBuilderExtensions
{
private const string PasswordEnvVarName = "ORACLE_PWD";
- ///
- /// Adds a Oracle Database resource to the application model. A container is used for local development. This version the package defaults to the 23.3.0.0 tag of the container-registry.oracle.com/database/free container image
- ///
- /// The .
- /// The name of the resource. This name will be used as the connection string name when referenced in a dependency.
- /// The host port for Oracle Database.
- /// The password for the Oracle Database container. Defaults to a random password.
- /// A reference to the .
- [Obsolete("Use AddOracle instead", error: true)]
- public static IResourceBuilder AddOracleDatabase(this IDistributedApplicationBuilder builder, string name, int? port = null, string? password = null)
- {
- // can't simulate the old behavior with taking a string password and converting it to a Parameter
- throw new NotSupportedException();
- }
-
///
/// Adds a Oracle Server resource to the application model. A container is used for local development. This version the package defaults to the 23.3.0.0 tag of the container-registry.oracle.com/database/free container image
///
@@ -42,7 +27,7 @@ public static IResourceBuilder AddOracle(this IDis
var oracleDatabaseServer = new OracleDatabaseServerResource(name, passwordParameter);
return builder.AddResource(oracleDatabaseServer)
- .WithEndpoint(hostPort: port, containerPort: 1521, name: OracleDatabaseServerResource.PrimaryEndpointName)
+ .WithEndpoint(port: port, targetPort: 1521, name: OracleDatabaseServerResource.PrimaryEndpointName)
.WithImage("database/free", "23.3.0.0")
.WithImageRegistry("container-registry.oracle.com")
.WithEnvironment(context =>
diff --git a/src/Aspire.Hosting.PostgreSQL/PostgresBuilderExtensions.cs b/src/Aspire.Hosting.PostgreSQL/PostgresBuilderExtensions.cs
index bc4745813e1..1d953115304 100644
--- a/src/Aspire.Hosting.PostgreSQL/PostgresBuilderExtensions.cs
+++ b/src/Aspire.Hosting.PostgreSQL/PostgresBuilderExtensions.cs
@@ -35,7 +35,7 @@ public static IResourceBuilder AddPostgres(this IDistrib
var postgresServer = new PostgresServerResource(name, userName?.Resource, passwordParameter);
return builder.AddResource(postgresServer)
- .WithEndpoint(hostPort: port, containerPort: 5432, name: PostgresServerResource.PrimaryEndpointName) // Internal port is always 5432.
+ .WithEndpoint(port: port, targetPort: 5432, name: PostgresServerResource.PrimaryEndpointName) // Internal port is always 5432.
.WithImage(PostgresContainerImageTags.Image, PostgresContainerImageTags.Tag)
.WithEnvironment("POSTGRES_HOST_AUTH_METHOD", "scram-sha-256")
.WithEnvironment("POSTGRES_INITDB_ARGS", "--auth-host=scram-sha-256 --auth-local=scram-sha-256")
@@ -84,7 +84,7 @@ public static IResourceBuilder WithPgAdmin(this IResourceBuilder builde
var pgAdminContainer = new PgAdminContainerResource(containerName);
builder.ApplicationBuilder.AddResource(pgAdminContainer)
.WithImage("dpage/pgadmin4", "8.3")
- .WithHttpEndpoint(containerPort: 80, hostPort: hostPort, name: containerName)
+ .WithHttpEndpoint(targetPort: 80, port: hostPort, name: containerName)
.WithEnvironment(SetPgAdminEnvironmentVariables)
.WithBindMount(Path.GetTempFileName(), "/pgadmin4/servers.json")
.ExcludeFromManifest();
diff --git a/src/Aspire.Hosting.RabbitMQ/RabbitMQBuilderExtensions.cs b/src/Aspire.Hosting.RabbitMQ/RabbitMQBuilderExtensions.cs
index feb9159f60a..0977e11d177 100644
--- a/src/Aspire.Hosting.RabbitMQ/RabbitMQBuilderExtensions.cs
+++ b/src/Aspire.Hosting.RabbitMQ/RabbitMQBuilderExtensions.cs
@@ -36,7 +36,7 @@ public static IResourceBuilder AddRabbitMQ(this IDistrib
var rabbitMq = new RabbitMQServerResource(name, userName?.Resource, passwordParameter);
var rabbitmq = builder.AddResource(rabbitMq)
.WithImage(RabbitMQContainerImageTags.Image, RabbitMQContainerImageTags.Tag)
- .WithEndpoint(hostPort: port, containerPort: 5672, name: RabbitMQServerResource.PrimaryEndpointName)
+ .WithEndpoint(port: port, targetPort: 5672, name: RabbitMQServerResource.PrimaryEndpointName)
.WithEnvironment(context =>
{
context.EnvironmentVariables["RABBITMQ_DEFAULT_USER"] = rabbitMq.UserNameReference;
@@ -127,7 +127,7 @@ public static IResourceBuilder WithManagementPlugin(this
if (handled)
{
- builder.WithHttpEndpoint(containerPort: 15672, name: RabbitMQServerResource.ManagementEndpointName);
+ builder.WithHttpEndpoint(targetPort: 15672, name: RabbitMQServerResource.ManagementEndpointName);
return builder;
}
diff --git a/src/Aspire.Hosting.Redis/RedisBuilderExtensions.cs b/src/Aspire.Hosting.Redis/RedisBuilderExtensions.cs
index 79c95541ae0..cfa6496f7ca 100644
--- a/src/Aspire.Hosting.Redis/RedisBuilderExtensions.cs
+++ b/src/Aspire.Hosting.Redis/RedisBuilderExtensions.cs
@@ -27,7 +27,7 @@ public static IResourceBuilder AddRedis(this IDistributedApplicat
{
var redis = new RedisResource(name);
return builder.AddResource(redis)
- .WithEndpoint(hostPort: port, containerPort: 6379, name: RedisResource.PrimaryEndpointName)
+ .WithEndpoint(port: port, targetPort: 6379, name: RedisResource.PrimaryEndpointName)
.WithImage(RedisContainerImageTags.Image, RedisContainerImageTags.Tag);
}
@@ -52,7 +52,7 @@ public static IResourceBuilder WithRedisCommander(this IResourceB
var resource = new RedisCommanderResource(containerName);
builder.ApplicationBuilder.AddResource(resource)
.WithImage("rediscommander/redis-commander", "latest")
- .WithHttpEndpoint(containerPort: 8081, hostPort: hostPort, name: containerName)
+ .WithHttpEndpoint(targetPort: 8081, port: hostPort, name: containerName)
.ExcludeFromManifest();
return builder;
diff --git a/src/Aspire.Hosting.Seq/SeqBuilderExtensions.cs b/src/Aspire.Hosting.Seq/SeqBuilderExtensions.cs
index fdeba93b44a..e1e24caee8b 100644
--- a/src/Aspire.Hosting.Seq/SeqBuilderExtensions.cs
+++ b/src/Aspire.Hosting.Seq/SeqBuilderExtensions.cs
@@ -28,7 +28,7 @@ public static IResourceBuilder AddSeq(
{
var seqResource = new SeqResource(name);
var resourceBuilder = builder.AddResource(seqResource)
- .WithHttpEndpoint(hostPort: port, containerPort: 80, name: SeqResource.PrimaryEndpointName)
+ .WithHttpEndpoint(port: port, targetPort: 80, name: SeqResource.PrimaryEndpointName)
.WithImage("datalust/seq", "2024.1")
.WithEnvironment("ACCEPT_EULA", "Y");
diff --git a/src/Aspire.Hosting.SqlServer/SqlServerBuilderExtensions.cs b/src/Aspire.Hosting.SqlServer/SqlServerBuilderExtensions.cs
index 64da515f5f3..f15ac9734d1 100644
--- a/src/Aspire.Hosting.SqlServer/SqlServerBuilderExtensions.cs
+++ b/src/Aspire.Hosting.SqlServer/SqlServerBuilderExtensions.cs
@@ -26,7 +26,7 @@ public static IResourceBuilder AddSqlServer(this IDistr
var sqlServer = new SqlServerServerResource(name, passwordParameter);
return builder.AddResource(sqlServer)
- .WithEndpoint(hostPort: port, containerPort: 1433, name: SqlServerServerResource.PrimaryEndpointName)
+ .WithEndpoint(port: port, targetPort: 1433, name: SqlServerServerResource.PrimaryEndpointName)
.WithImage(SqlServerContainerImageTags.Image, SqlServerContainerImageTags.Tag)
.WithImageRegistry(SqlServerContainerImageTags.Registry)
.WithEnvironment("ACCEPT_EULA", "Y")
diff --git a/src/Aspire.Hosting.Testing/Properties/xlf/Resources.cs.xlf b/src/Aspire.Hosting.Testing/Properties/xlf/Resources.cs.xlf
index 23e44985197..61efe72e431 100644
--- a/src/Aspire.Hosting.Testing/Properties/xlf/Resources.cs.xlf
+++ b/src/Aspire.Hosting.Testing/Properties/xlf/Resources.cs.xlf
@@ -4,27 +4,27 @@
- The application must be started before resolving endpoints or connection strings.
+ Aplikace musí být spuštěna před překladem koncových bodů nebo připojovacích řetězců.
- Endpoint '{0}' for resource '{1}' not found.
+ Koncový bod {0} pro prostředek {1} se nenašel.
- Resource '{0}' does not expose a connection string.
+ Prostředek {0} nezpřístupňuje připojovací řetězec.
- Resource '{0}' has no allocated endpoints.
+ Prostředek {0} nemá žádné přidělené koncové body.
- Resource '{0}' not found.
+ Prostředek {0} nebyl nalezen.
diff --git a/src/Aspire.Hosting.Testing/Properties/xlf/Resources.pt-BR.xlf b/src/Aspire.Hosting.Testing/Properties/xlf/Resources.pt-BR.xlf
index 2b86834e1ac..4bae1d1e624 100644
--- a/src/Aspire.Hosting.Testing/Properties/xlf/Resources.pt-BR.xlf
+++ b/src/Aspire.Hosting.Testing/Properties/xlf/Resources.pt-BR.xlf
@@ -4,27 +4,27 @@
- The application must be started before resolving endpoints or connection strings.
+ O aplicativo deve ser iniciado antes de resolver pontos de extremidade ou cadeias de conexão.
- Endpoint '{0}' for resource '{1}' not found.
+ O ponto de extremidade “{0}” para o recurso “{1}” não foi encontrado.
- Resource '{0}' does not expose a connection string.
+ O recurso “{0}” não expõe uma cadeia de conexão.
- Resource '{0}' has no allocated endpoints.
+ O recurso “{0}” não tem pontos de extremidade alocados.
- Resource '{0}' not found.
+ O recurso “{0}” não foi encontrado.
diff --git a/src/Aspire.Hosting.Testing/Properties/xlf/Resources.tr.xlf b/src/Aspire.Hosting.Testing/Properties/xlf/Resources.tr.xlf
index 92a863430b1..3e26aac0c56 100644
--- a/src/Aspire.Hosting.Testing/Properties/xlf/Resources.tr.xlf
+++ b/src/Aspire.Hosting.Testing/Properties/xlf/Resources.tr.xlf
@@ -4,27 +4,27 @@
- The application must be started before resolving endpoints or connection strings.
+ Uç noktalarını veya bağlantı dizelerini çözümlemeden önce uygulama başlatmalıdır.
- Endpoint '{0}' for resource '{1}' not found.
+ '{1}' kaynağı için '{0}' uç noktası bulunamadı.
- Resource '{0}' does not expose a connection string.
+ '{0}' kaynağı, bir bağlantı dizesi göstermez.
- Resource '{0}' has no allocated endpoints.
+ '{0}' kaynağında ayrılmış uç nokta yok.
- Resource '{0}' not found.
+ '{0}' kaynağı bulunamadı.
diff --git a/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs
index 3ebc67a2aaf..e7a4a39ad69 100644
--- a/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs
+++ b/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs
@@ -24,11 +24,11 @@ public sealed class EndpointAnnotation : IResourceAnnotation
/// Transport that is being used (e.g. http, http2, http3 etc).
/// Name of the service.
/// Desired port for the service.
- /// If the endpoint is used for the container, this is the port the container process is listening on.
+ /// This is the port the resource is listening on. If the endpoint is used for the container, it is the container port.
/// Indicates that this endpoint should be exposed externally at publish time.
/// The name of the environment variable that will be set to the port number of this endpoint.
/// Specifies if the endpoint will be proxied by DCP. Defaults to true.
- public EndpointAnnotation(ProtocolType protocol, string? uriScheme = null, string? transport = null, string? name = null, int? port = null, int? containerPort = null, bool? isExternal = null, string? env = null, bool isProxied = true)
+ public EndpointAnnotation(ProtocolType protocol, string? uriScheme = null, string? transport = null, string? name = null, int? port = null, int? targetPort = null, bool? isExternal = null, string? env = null, bool isProxied = true)
{
// If the URI scheme is null, we'll adopt either udp:// or tcp:// based on the
// protocol. If the name is null, we'll use the URI scheme as the default. This
@@ -45,7 +45,7 @@ public EndpointAnnotation(ProtocolType protocol, string? uriScheme = null, strin
_transport = transport;
Name = name;
Port = port;
- ContainerPort = containerPort ?? port;
+ TargetPort = targetPort ?? port;
IsExternal = isExternal ?? false;
EnvironmentVariable = env;
IsProxied = isProxied;
@@ -67,12 +67,12 @@ public EndpointAnnotation(ProtocolType protocol, string? uriScheme = null, strin
public int? Port { get; set; }
///
- /// If the endpoint is used for the container, this is the port the container process is listening on.
+ /// This is the port the resource is listening on. If the endpoint is used for the container, it is the container port.
///
///
/// Defaults to .
///
- public int? ContainerPort { get; set; }
+ public int? TargetPort { get; set; }
///
/// If a service is URI-addressable, this property will contain the URI scheme to use for constructing service URI.
diff --git a/src/Aspire.Hosting/ApplicationModel/EndpointAnnotationExtensions.cs b/src/Aspire.Hosting/ApplicationModel/EndpointAnnotationExtensions.cs
deleted file mode 100644
index 7fa680e48e8..00000000000
--- a/src/Aspire.Hosting/ApplicationModel/EndpointAnnotationExtensions.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace Aspire.Hosting.ApplicationModel;
-
-///
-/// Provides extension methods for .
-///
-public static class EndpointAnnotationExtensions
-{
- ///
- /// Sets the transport to HTTP/2 and the URI scheme to HTTPS for the specified object.
- ///
- /// The object to modify.
- /// The modified object.
- public static EndpointAnnotation AsHttp2(this EndpointAnnotation endpoint)
- {
- ArgumentNullException.ThrowIfNull(endpoint);
-
- endpoint.Transport = "http2";
- endpoint.UriScheme = "https";
- return endpoint;
- }
-
- ///
- /// Sets the property to true for the specified object.
- ///
- /// The object to modify.
- /// The modified object.
- public static EndpointAnnotation AsExternal(this EndpointAnnotation endpoint)
- {
- ArgumentNullException.ThrowIfNull(endpoint);
-
- endpoint.IsExternal = true;
- return endpoint;
- }
-}
diff --git a/src/Aspire.Hosting/ApplicationModel/EnvironmentCallbackContext.cs b/src/Aspire.Hosting/ApplicationModel/EnvironmentCallbackContext.cs
index 5285e5daae2..12afa8e705d 100644
--- a/src/Aspire.Hosting/ApplicationModel/EnvironmentCallbackContext.cs
+++ b/src/Aspire.Hosting/ApplicationModel/EnvironmentCallbackContext.cs
@@ -13,12 +13,6 @@ namespace Aspire.Hosting.ApplicationModel;
/// A .
public class EnvironmentCallbackContext(DistributedApplicationExecutionContext executionContext, Dictionary? environmentVariables = null, CancellationToken cancellationToken = default)
{
- ///
- /// Obsolete. Use ExecutionContext instead. Will be removed in next preview.
- ///
- [Obsolete("Use ExecutionContext instead")]
- public string PublisherName => ExecutionContext.IsPublishMode ? "manifest" : "dcp";
-
///
/// Gets the environment variables associated with the callback context.
///
diff --git a/src/Aspire.Hosting/ApplicationModel/ResourceLoggerService.cs b/src/Aspire.Hosting/ApplicationModel/ResourceLoggerService.cs
index b9b793df6d9..85e0e805e56 100644
--- a/src/Aspire.Hosting/ApplicationModel/ResourceLoggerService.cs
+++ b/src/Aspire.Hosting/ApplicationModel/ResourceLoggerService.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Concurrent;
+using System.Runtime.CompilerServices;
using System.Threading.Channels;
using Aspire.Dashboard.Otlp.Storage;
using Microsoft.Extensions.Logging;
@@ -15,11 +16,29 @@ public class ResourceLoggerService
{
private readonly ConcurrentDictionary _loggers = new();
+ private Action<(string, ResourceLoggerState)>? _loggerAdded;
+ private event Action<(string, ResourceLoggerState)> LoggerAdded
+ {
+ add
+ {
+ _loggerAdded += value;
+
+ foreach (var logger in _loggers)
+ {
+ value((logger.Key, logger.Value));
+ }
+ }
+ remove
+ {
+ _loggerAdded -= value;
+ }
+ }
+
///
/// Gets the logger for the resource to write to.
///
/// The resource name
- /// An .
+ /// An which represents the resource.
public ILogger GetLogger(IResource resource)
{
ArgumentNullException.ThrowIfNull(resource);
@@ -31,7 +50,7 @@ public ILogger GetLogger(IResource resource)
/// Gets the logger for the resource to write to.
///
/// The name of the resource from the Aspire application model.
- /// An which repesents the named resource.
+ /// An which represents the named resource.
public ILogger GetLogger(string resourceName)
{
ArgumentNullException.ThrowIfNull(resourceName);
@@ -39,11 +58,23 @@ public ILogger GetLogger(string resourceName)
return GetResourceLoggerState(resourceName).Logger;
}
+ ///
+ /// Watch for changes to the log stream for a resource.
+ ///
+ /// The resource to watch for logs.
+ /// An async enumerable that returns the logs as they are written.
+ public IAsyncEnumerable> WatchAsync(IResource resource)
+ {
+ ArgumentNullException.ThrowIfNull(resource);
+
+ return WatchAsync(resource.Name);
+ }
+
///
/// Watch for changes to the log stream for a resource.
///
/// The resource name
- ///
+ /// An async enumerable that returns the logs as they are written.
public IAsyncEnumerable> WatchAsync(string resourceName)
{
ArgumentNullException.ThrowIfNull(resourceName);
@@ -52,15 +83,39 @@ public IAsyncEnumerable> WatchAsync(string resourceName)
}
///
- /// Watch for changes to the log stream for a resource.
+ /// Watch for subscribers to the log stream for a resource.
///
- /// The resource to watch for logs.
- ///
- public IAsyncEnumerable> WatchAsync(IResource resource)
+ ///
+ /// An async enumerable that returns when the first subscriber is added to a log,
+ /// or when the last subscriber is removed.
+ ///
+ public async IAsyncEnumerable WatchAnySubscribersAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
{
- ArgumentNullException.ThrowIfNull(resource);
+ var channel = Channel.CreateUnbounded();
- return WatchAsync(resource.Name);
+ void OnLoggerAdded((string Name, ResourceLoggerState State) loggerItem)
+ {
+ var (name, state) = loggerItem;
+
+ state.OnSubscribersChanged += (hasSubscribers) =>
+ {
+ channel.Writer.TryWrite(new(name, hasSubscribers));
+ };
+ }
+
+ LoggerAdded += OnLoggerAdded;
+
+ try
+ {
+ await foreach (var entry in channel.Reader.ReadAllAsync(cancellationToken))
+ {
+ yield return entry;
+ }
+ }
+ finally
+ {
+ LoggerAdded -= OnLoggerAdded;
+ }
}
///
@@ -91,8 +146,27 @@ public void Complete(string name)
}
}
+ ///
+ /// Clears the log stream's backlog for the resource.
+ ///
+ public void ClearBacklog(string resourceName)
+ {
+ ArgumentNullException.ThrowIfNull(resourceName);
+
+ if (_loggers.TryGetValue(resourceName, out var logger))
+ {
+ logger.ClearBacklog();
+ }
+ }
+
private ResourceLoggerState GetResourceLoggerState(string resourceName) =>
- _loggers.GetOrAdd(resourceName, _ => new ResourceLoggerState());
+ _loggers.GetOrAdd(resourceName, (name, context) =>
+ {
+ var state = new ResourceLoggerState();
+ context._loggerAdded?.Invoke((name, state));
+ return state;
+ },
+ this);
///
/// A logger for the resource to write to.
@@ -102,7 +176,6 @@ private sealed class ResourceLoggerState
private readonly ResourceLogger _logger;
private readonly CancellationTokenSource _logStreamCts = new();
- // History of logs, capped at 10000 entries.
private readonly CircularBuffer _backlog = new(10000);
///
@@ -113,21 +186,112 @@ public ResourceLoggerState()
_logger = new ResourceLogger(this);
}
+ private Action? _onSubscribersChanged;
+ public event Action OnSubscribersChanged
+ {
+ add
+ {
+ _onSubscribersChanged += value;
+
+ var hasSubscribers = false;
+
+ lock (this)
+ {
+ if (_onNewLog is not null) // we have subscribers
+ {
+ hasSubscribers = true;
+ }
+ }
+
+ if (hasSubscribers)
+ {
+ value(hasSubscribers);
+ }
+ }
+ remove
+ {
+ _onSubscribersChanged -= value;
+ }
+ }
+
///
/// Watch for changes to the log stream for a resource.
///
/// The log stream for the resource.
- public IAsyncEnumerable> WatchAsync()
+ public async IAsyncEnumerable> WatchAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
{
- lock (_backlog)
+ var channel = Channel.CreateUnbounded();
+
+ using var _ = _logStreamCts.Token.Register(() => channel.Writer.TryComplete());
+
+ void Log(LogLine log)
{
- // REVIEW: Performance makes me very sad, but we can optimize this later.
- return new LogAsyncEnumerable(this, _backlog.ToList());
+ channel.Writer.TryWrite(log);
+ }
+
+ OnNewLog += Log;
+
+ // ensure the backlog snapshot is taken after subscribing to OnNewLog
+ // to ensure the backlog snapshot contains the correct logs. The backlog
+ // can get cleared when there are no subscribers, so we ensure we are subscribing first.
+
+ // REVIEW: Performance makes me very sad, but we can optimize this later.
+ var backlogSnapshot = GetBacklogSnapshot();
+ if (backlogSnapshot.Length > 0)
+ {
+ yield return backlogSnapshot;
+ }
+
+ try
+ {
+ await foreach (var entry in channel.GetBatchesAsync(cancellationToken: cancellationToken))
+ {
+ yield return entry;
+ }
+ }
+ finally
+ {
+ OnNewLog -= Log;
+
+ channel.Writer.TryComplete();
}
}
// This provides the fan out to multiple subscribers.
- private Action? OnNewLog { get; set; }
+ private Action? _onNewLog;
+ private event Action OnNewLog
+ {
+ add
+ {
+ bool raiseSubscribersChanged;
+ lock (this)
+ {
+ raiseSubscribersChanged = _onNewLog is null; // is this the first subscriber?
+
+ _onNewLog += value;
+ }
+
+ if (raiseSubscribersChanged)
+ {
+ _onSubscribersChanged?.Invoke(true);
+ }
+ }
+ remove
+ {
+ bool raiseSubscribersChanged;
+ lock (this)
+ {
+ _onNewLog -= value;
+
+ raiseSubscribersChanged = _onNewLog is null; // is this the last subscriber?
+ }
+
+ if (raiseSubscribersChanged)
+ {
+ _onSubscribersChanged?.Invoke(false);
+ }
+ }
+ }
///
/// The logger for the resource to write to. This will write updates to the live log stream for this resource.
@@ -143,7 +307,23 @@ public void Complete()
_logStreamCts.Cancel();
}
- private sealed class ResourceLogger(ResourceLoggerState annotation) : ILogger
+ public void ClearBacklog()
+ {
+ lock (_backlog)
+ {
+ _backlog.Clear();
+ }
+ }
+
+ private LogLine[] GetBacklogSnapshot()
+ {
+ lock (_backlog)
+ {
+ return [.. _backlog];
+ }
+ }
+
+ private sealed class ResourceLogger(ResourceLoggerState loggerState) : ILogger
{
private int _lineNumber;
@@ -153,7 +333,7 @@ private sealed class ResourceLogger(ResourceLoggerState annotation) : ILogger
public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter)
{
- if (annotation._logStreamCts.IsCancellationRequested)
+ if (loggerState._logStreamCts.IsCancellationRequested)
{
// Noop if logging after completing the stream
return;
@@ -163,52 +343,23 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except
var isErrorMessage = logLevel >= LogLevel.Error;
LogLine logLine;
- lock (annotation._backlog)
+ lock (loggerState._backlog)
{
_lineNumber++;
logLine = new LogLine(_lineNumber, log, isErrorMessage);
- annotation._backlog.Add(logLine);
+ loggerState._backlog.Add(logLine);
}
- annotation.OnNewLog?.Invoke(logLine);
- }
- }
-
- private sealed class LogAsyncEnumerable(ResourceLoggerState annotation, List backlogSnapshot) : IAsyncEnumerable>
- {
- public async IAsyncEnumerator> GetAsyncEnumerator(CancellationToken cancellationToken = default)
- {
- if (backlogSnapshot.Count > 0)
- {
- yield return backlogSnapshot;
- }
-
- var channel = Channel.CreateUnbounded();
-
- using var _ = annotation._logStreamCts.Token.Register(() => channel.Writer.TryComplete());
-
- void Log(LogLine log)
- {
- channel.Writer.TryWrite(log);
- }
-
- annotation.OnNewLog += Log;
-
- try
- {
- await foreach (var entry in channel.GetBatchesAsync(cancellationToken: cancellationToken))
- {
- yield return entry;
- }
- }
- finally
- {
- annotation.OnNewLog -= Log;
-
- channel.Writer.TryComplete();
- }
+ loggerState._onNewLog?.Invoke(logLine);
}
}
}
}
+
+///
+/// Represents a log subscriber for a resource.
+///
+/// The the resource name.
+/// Determines if there are any subscribers.
+public readonly record struct LogSubscriber(string Name, bool AnySubscribers);
diff --git a/src/Aspire.Hosting/ApplicationModel/ResourceNotificationService.cs b/src/Aspire.Hosting/ApplicationModel/ResourceNotificationService.cs
index 2de24717016..3988eeb26e2 100644
--- a/src/Aspire.Hosting/ApplicationModel/ResourceNotificationService.cs
+++ b/src/Aspire.Hosting/ApplicationModel/ResourceNotificationService.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Concurrent;
+using System.Runtime.CompilerServices;
using System.Threading.Channels;
using Microsoft.Extensions.Logging;
@@ -22,8 +23,40 @@ public class ResourceNotificationService(ILogger lo
/// Watch for changes to the state for all resources.
///
///
- public IAsyncEnumerable WatchAsync() =>
- new AllResourceUpdatesAsyncEnumerable(this);
+ public async IAsyncEnumerable WatchAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ // Return the last snapshot for each resource.
+ foreach (var state in _resourceNotificationStates)
+ {
+ var (resource, resourceId) = state.Key;
+
+ if (state.Value.LastSnapshot is not null)
+ {
+ yield return new ResourceEvent(resource, resourceId, state.Value.LastSnapshot);
+ }
+ }
+
+ var channel = Channel.CreateUnbounded();
+
+ void WriteToChannel(ResourceEvent resourceEvent) =>
+ channel.Writer.TryWrite(resourceEvent);
+
+ OnResourceUpdated += WriteToChannel;
+
+ try
+ {
+ await foreach (var item in channel.Reader.ReadAllAsync(cancellationToken))
+ {
+ yield return item;
+ }
+ }
+ finally
+ {
+ OnResourceUpdated -= WriteToChannel;
+
+ channel.Writer.TryComplete();
+ }
+ }
///
/// Updates the snapshot of the for a resource.
@@ -37,9 +70,9 @@ public Task PublishUpdateAsync(IResource resource, string resourceId, Func
_resourceNotificationStates.GetOrAdd((resource, resourceId), _ => new ResourceNotificationState());
- private sealed class AllResourceUpdatesAsyncEnumerable(ResourceNotificationService resourceNotificationService) : IAsyncEnumerable
- {
- public async IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default)
- {
- // Return the last snapshot for each resource.
- foreach (var state in resourceNotificationService._resourceNotificationStates)
- {
- var (resource, resourceId) = state.Key;
-
- if (state.Value.LastSnapshot is not null)
- {
- yield return new ResourceEvent(resource, resourceId, state.Value.LastSnapshot);
- }
- }
-
- var channel = Channel.CreateUnbounded();
-
- void WriteToChannel(ResourceEvent resourceEvent) =>
- channel.Writer.TryWrite(resourceEvent);
-
- resourceNotificationService.OnResourceUpdated += WriteToChannel;
-
- try
- {
- await foreach (var item in channel.Reader.ReadAllAsync(cancellationToken))
- {
- yield return item;
- }
- }
- finally
- {
- resourceNotificationService.OnResourceUpdated -= WriteToChannel;
-
- channel.Writer.TryComplete();
- }
- }
- }
-
///
/// The annotation that allows publishing and subscribing to changes in the state of a resource.
///
diff --git a/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs b/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs
index 132fae6b458..7ad36e32300 100644
--- a/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs
+++ b/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs
@@ -6,6 +6,7 @@
using System.Diagnostics;
using System.Net.Sockets;
using System.Text.Json;
+using System.Threading.Channels;
using Aspire.Dashboard.Model;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Dashboard;
@@ -92,6 +93,10 @@ internal sealed class ApplicationExecutor(ILogger logger,
private readonly ConcurrentDictionary _hiddenResources = new();
private DcpInfo? _dcpInfo;
+ private readonly record struct LogInformationEntry(string ResourceName, bool? LogsAvailable, bool? HasSubscribers);
+ private readonly Channel _logInformationChannel = Channel.CreateUnbounded(
+ new UnboundedChannelOptions { SingleReader = true });
+
private string DefaultContainerHostName => configuration["AppHost:ContainerHostname"] ?? _dcpInfo?.Containers?.ContainerHostName ?? "host.docker.internal";
public async Task RunApplicationAsync(CancellationToken cancellationToken = default)
@@ -195,6 +200,73 @@ await Task.WhenAll(
},
cancellationToken);
+ Task.Run(async () =>
+ {
+ await foreach (var subscribers in loggerService.WatchAnySubscribersAsync())
+ {
+ _logInformationChannel.Writer.TryWrite(new(subscribers.Name, LogsAvailable: null, subscribers.AnySubscribers));
+ }
+ },
+ cancellationToken);
+
+ // Listen to the "log information channel" - which contains updates when resources have logs available and when they have subscribers.
+ // A resource needs both logs available and subscribers before it starts streaming its logs.
+ // We only want to start the log stream for resources when they have subscribers.
+ // And when there are no more subscribers, we want to stop the stream.
+ Task.Run(async () =>
+ {
+ var resourceLogState = new Dictionary();
+
+ await foreach (var entry in _logInformationChannel.Reader.ReadAllAsync(cancellationToken))
+ {
+ var logsAvailable = false;
+ var hasSubscribers = false;
+ if (resourceLogState.TryGetValue(entry.ResourceName, out (bool, bool) stateEntry))
+ {
+ (logsAvailable, hasSubscribers) = stateEntry;
+ }
+
+ // LogsAvailable can only go from false => true. Once it is true, it can never go back to false.
+ Debug.Assert(!entry.LogsAvailable.HasValue || entry.LogsAvailable.Value, "entry.LogsAvailable should never be 'false'");
+
+ logsAvailable = entry.LogsAvailable ?? logsAvailable;
+ hasSubscribers = entry.HasSubscribers ?? hasSubscribers;
+
+ if (logsAvailable)
+ {
+ if (hasSubscribers)
+ {
+ if (_containersMap.TryGetValue(entry.ResourceName, out var container))
+ {
+ StartLogStream(container);
+ }
+ else if (_executablesMap.TryGetValue(entry.ResourceName, out var executable))
+ {
+ StartLogStream(executable);
+ }
+ }
+ else
+ {
+ if (_logStreams.TryRemove(entry.ResourceName, out var cts))
+ {
+ cts.Cancel();
+ }
+
+ if (_containersMap.TryGetValue(entry.ResourceName, out var _) ||
+ _executablesMap.TryGetValue(entry.ResourceName, out var _))
+ {
+ // Clear out the backlog for containers and executables after the last subscriber leaves.
+ // When a new subscriber is added, the full log will be replayed.
+ loggerService.ClearBacklog(entry.ResourceName);
+ }
+ }
+ }
+
+ resourceLogState[entry.ResourceName] = (logsAvailable, hasSubscribers);
+ }
+ },
+ cancellationToken);
+
async Task WatchKubernetesResource(Func handler) where T : CustomResource
{
var retryUntilCancelled = new RetryStrategyOptions()
@@ -273,6 +345,9 @@ private async Task ProcessResourceChange(WatchEventType watchEventType, T res
cts.Cancel();
}
+ // Complete the log stream
+ loggerService.Complete(resource.Metadata.Name);
+
// TODO: Handle resource deletion
if (_logger.IsEnabled(LogLevel.Trace))
{
@@ -295,7 +370,11 @@ private async Task ProcessResourceChange(WatchEventType watchEventType, T res
// Notifications are associated with the application model resource, so we need to update with that context
await notificationService.PublishUpdateAsync(appModelResource, resource.Metadata.Name, s => snapshotFactory(resource, s)).ConfigureAwait(false);
- StartLogStream(resource);
+ if (resource is Container { LogsAvailable: true } ||
+ resource is Executable { LogsAvailable: true })
+ {
+ _logInformationChannel.Writer.TryWrite(new(resource.Metadata.Name, LogsAvailable: true, HasSubscribers: null));
+ }
}
// Update all child resources of containers
@@ -385,11 +464,6 @@ private void StartLogStream(T resource) where T : CustomResource
{
_logger.LogError(ex, "Error streaming logs for {ResourceName}", resource.Metadata.Name);
}
- finally
- {
- // Complete the log stream
- loggerService.Complete(resource.Metadata.Name);
- }
},
cts.Token);
@@ -774,8 +848,11 @@ private async Task>> GetDashboardEnvironmentVa
var resourceServiceUrl = await _dashboardEndpointProvider.GetResourceServiceUriAsync(cancellationToken).ConfigureAwait(false);
+ var environment = configuration["ASPNETCORE_ENVIRONMENT"] ?? "Production";
+
var env = new List>
{
+ KeyValuePair.Create("ASPNETCORE_ENVIRONMENT", environment),
KeyValuePair.Create(DashboardConfigNames.DashboardFrontendUrlName.EnvVarName, dashboardUrls),
KeyValuePair.Create(DashboardConfigNames.ResourceServiceUrlName.EnvVarName, resourceServiceUrl),
KeyValuePair.Create(DashboardConfigNames.DashboardOtlpUrlName.EnvVarName, otlpEndpointUrl),
@@ -995,19 +1072,30 @@ private void PrepareProjectExecutables()
{
exeSpec.ExecutionType = ExecutionType.IDE;
-#pragma warning disable CS0612 // These annotations are obsolete; remove in Aspire Preview 6
- annotationHolder.Annotate(Executable.CSharpProjectPathAnnotation, projectMetadata.ProjectPath);
-
- // ExcludeLaunchProfileAnnotation takes precedence over LaunchProfileAnnotation.
- if (project.TryGetLastAnnotation(out _))
+ if (_dcpInfo?.Version?.CompareTo(DcpVersion.MinimumVersionIdeProtocolV1) >= 0)
{
- annotationHolder.Annotate(Executable.CSharpDisableLaunchProfileAnnotation, "true");
+ projectLaunchConfiguration.DisableLaunchProfile = project.TryGetLastAnnotation(out _);
+ if (!projectLaunchConfiguration.DisableLaunchProfile && project.TryGetLastAnnotation(out var lpa))
+ {
+ projectLaunchConfiguration.LaunchProfile = lpa.LaunchProfileName;
+ }
}
- else if (project.TryGetLastAnnotation(out var lpa))
+ else
{
- annotationHolder.Annotate(Executable.CSharpLaunchProfileAnnotation, lpa.LaunchProfileName);
- }
+#pragma warning disable CS0612 // These annotations are obsolete; remove in Aspire Preview 6
+ annotationHolder.Annotate(Executable.CSharpProjectPathAnnotation, projectMetadata.ProjectPath);
+
+ // ExcludeLaunchProfileAnnotation takes precedence over LaunchProfileAnnotation.
+ if (project.TryGetLastAnnotation(out _))
+ {
+ annotationHolder.Annotate(Executable.CSharpDisableLaunchProfileAnnotation, "true");
+ }
+ else if (project.TryGetLastAnnotation(out var lpa))
+ {
+ annotationHolder.Annotate(Executable.CSharpLaunchProfileAnnotation, lpa.LaunchProfileName);
+ }
#pragma warning restore CS0612
+ }
}
else
{
@@ -1098,6 +1186,13 @@ await notificationService.PublishUpdateAsync(cr.ModelResource, s => s with
{
await CreateExecutableAsync(cr, logger, cancellationToken).ConfigureAwait(false);
}
+ catch (FailedToApplyEnvironmentException)
+ {
+ // For this exception we don't want the noise of the stack trace, we've already
+ // provided more detail where we detected the issue (e.g. envvar name). To get
+ // more diagnostic information reduce logging level for DCP log category to Debug.
+ await notificationService.PublishUpdateAsync(cr.ModelResource, s => s with { State = "FailedToStart" }).ConfigureAwait(false);
+ }
catch (Exception ex)
{
logger.LogError(ex, "Failed to create resource {ResourceName}", cr.ModelResource.Name);
@@ -1139,6 +1234,7 @@ private async Task CreateExecutableAsync(AppResource er, ILogger resourceLogger,
throw new InvalidOperationException($"Expected an Executable-like resource, but got {er.DcpResource.Kind} instead");
}
+ bool failedToApplyArgs = false;
spec.Args ??= [];
if (er.ModelResource.TryGetAnnotationsOfType(out var exeArgsCallbacks))
@@ -1153,17 +1249,26 @@ private async Task CreateExecutableAsync(AppResource er, ILogger resourceLogger,
foreach (var arg in args)
{
- var value = arg switch
+ try
{
- string s => s,
- IValueProvider valueProvider => await GetValue(key: null, valueProvider, resourceLogger, isContainer: false, cancellationToken).ConfigureAwait(false),
- null => null,
- _ => throw new InvalidOperationException($"Unexpected value for {arg}")
- };
+ var value = arg switch
+ {
+ string s => s,
+ IValueProvider valueProvider => await GetValue(key: null, valueProvider, resourceLogger, isContainer: false, cancellationToken).ConfigureAwait(false),
+ null => null,
+ _ => throw new InvalidOperationException($"Unexpected value for {arg}")
+ };
- if (value is not null)
+ if (value is not null)
+ {
+ spec.Args.Add(value);
+ }
+ }
+ catch (Exception ex)
{
- spec.Args.Add(value);
+ resourceLogger.LogCritical("Failed to apply arguments. A dependency may have failed to start.");
+ _logger.LogDebug(ex, "Failed to apply arguments. A dependency may have failed to start.");
+ failedToApplyArgs = true;
}
}
}
@@ -1208,23 +1313,38 @@ private async Task CreateExecutableAsync(AppResource er, ILogger resourceLogger,
}
}
+ bool failedToApplyConfiguration = false;
spec.Env = [];
foreach (var c in config)
{
- var value = c.Value switch
+ try
{
- string s => s,
- IValueProvider valueProvider => await GetValue(c.Key, valueProvider, resourceLogger, isContainer: false, cancellationToken).ConfigureAwait(false),
- null => null,
- _ => throw new InvalidOperationException($"Unexpected value for environment variable \"{c.Key}\".")
- };
+ var value = c.Value switch
+ {
+ string s => s,
+ IValueProvider valueProvider => await GetValue(c.Key, valueProvider, resourceLogger, isContainer: false, cancellationToken).ConfigureAwait(false),
+ null => null,
+ _ => throw new InvalidOperationException($"Unexpected value for environment variable \"{c.Key}\".")
+ };
- if (value is not null)
+ if (value is not null)
+ {
+ spec.Env.Add(new EnvVar { Name = c.Key, Value = value });
+ }
+ }
+ catch (Exception ex)
{
- spec.Env.Add(new EnvVar { Name = c.Key, Value = value });
+ resourceLogger.LogCritical("Failed to apply configuration value '{ConfigKey}'. A dependency may have failed to start.", c.Key);
+ _logger.LogDebug(ex, "Failed to apply configuration value '{ConfigKey}'. A dependency may have failed to start.", c.Key);
+ failedToApplyConfiguration = true;
}
}
+ if (failedToApplyConfiguration || failedToApplyArgs)
+ {
+ throw new FailedToApplyEnvironmentException();
+ }
+
await createResource().ConfigureAwait(false);
// NOTE: This check is only necessary for the inner loop in the dotnet/aspire repo. When
@@ -1438,6 +1558,13 @@ await notificationService.PublishUpdateAsync(cr.ModelResource, s => s with
{
await CreateContainerAsync(cr, logger, cancellationToken).ConfigureAwait(false);
}
+ catch (FailedToApplyEnvironmentException)
+ {
+ // For this exception we don't want the noise of the stack trace, we've already
+ // provided more detail where we detected the issue (e.g. envvar name). To get
+ // more diagnostic information reduce logging level for DCP log category to Debug.
+ await notificationService.PublishUpdateAsync(cr.ModelResource, s => s with { State = "FailedToStart" }).ConfigureAwait(false);
+ }
catch (Exception ex)
{
logger.LogError(ex, "Failed to create container resource {ResourceName}", cr.ModelResource.Name);
@@ -1523,22 +1650,33 @@ private async Task CreateContainerAsync(AppResource cr, ILogger resourceLogger,
}
}
+ bool failedToApplyConfiguration = false;
foreach (var kvp in config)
{
- var value = kvp.Value switch
+ try
{
- string s => s,
- IValueProvider valueProvider => await GetValue(kvp.Key, valueProvider, resourceLogger, isContainer: true, cancellationToken).ConfigureAwait(false),
- null => null,
- _ => throw new InvalidOperationException($"Unexpected value for environment variable \"{kvp.Key}\".")
- };
+ var value = kvp.Value switch
+ {
+ string s => s,
+ IValueProvider valueProvider => await GetValue(kvp.Key, valueProvider, resourceLogger, isContainer: true, cancellationToken).ConfigureAwait(false),
+ null => null,
+ _ => throw new InvalidOperationException($"Unexpected value for environment variable \"{kvp.Key}\".")
+ };
- if (value is not null)
+ if (value is not null)
+ {
+ dcpContainerResource.Spec.Env.Add(new EnvVar { Name = kvp.Key, Value = value });
+ }
+ }
+ catch (Exception ex)
{
- dcpContainerResource.Spec.Env.Add(new EnvVar { Name = kvp.Key, Value = value });
+ resourceLogger.LogCritical("Failed to apply configuration value '{ConfigKey}'. A dependency may have failed to start.", kvp.Key);
+ _logger.LogDebug(ex, "Failed to apply configuration value '{ConfigKey}'. A dependency may have failed to start.", kvp.Key);
+ failedToApplyConfiguration = true;
}
}
+ var failedToApplyArgs = false;
if (modelContainerResource.TryGetAnnotationsOfType(out var argsCallback))
{
dcpContainerResource.Spec.Args ??= [];
@@ -1554,17 +1692,26 @@ private async Task CreateContainerAsync(AppResource cr, ILogger resourceLogger,
foreach (var arg in args)
{
- var value = arg switch
+ try
{
- string s => s,
- IValueProvider valueProvider => await GetValue(key: null, valueProvider, resourceLogger, isContainer: true, cancellationToken).ConfigureAwait(false),
- null => null,
- _ => throw new InvalidOperationException($"Unexpected value for {arg}")
- };
+ var value = arg switch
+ {
+ string s => s,
+ IValueProvider valueProvider => await GetValue(key: null, valueProvider, resourceLogger, isContainer: true, cancellationToken).ConfigureAwait(false),
+ null => null,
+ _ => throw new InvalidOperationException($"Unexpected value for {arg}")
+ };
- if (value is not null)
+ if (value is not null)
+ {
+ dcpContainerResource.Spec.Args.Add(value);
+ }
+ }
+ catch (Exception ex)
{
- dcpContainerResource.Spec.Args.Add(value);
+ resourceLogger.LogCritical("Failed to apply container arguments '{ConfigKey}'. A dependency may have failed to start.", arg);
+ _logger.LogDebug(ex, "Failed to apply container arguments '{ConfigKey}'. A dependency may have failed to start.", arg);
+ failedToApplyArgs = true;
}
}
}
@@ -1574,6 +1721,11 @@ private async Task CreateContainerAsync(AppResource cr, ILogger resourceLogger,
dcpContainerResource.Spec.Command = containerResource.Entrypoint;
}
+ if (failedToApplyArgs || failedToApplyConfiguration)
+ {
+ throw new FailedToApplyEnvironmentException();
+ }
+
await kubernetesService.CreateAsync(dcpContainerResource, cancellationToken).ConfigureAwait(false);
}
@@ -1595,12 +1747,12 @@ private void AddServicesProducedInfo(IResource modelResource, IAnnotationHolder
if (modelResource.IsContainer())
{
- if (sp.EndpointAnnotation.ContainerPort is null)
+ if (sp.EndpointAnnotation.TargetPort is null)
{
throw new InvalidOperationException($"The endpoint for container resource {modelResourceName} must specify the ContainerPort");
}
- sp.DcpServiceProducerAnnotation.Port = sp.EndpointAnnotation.ContainerPort;
+ sp.DcpServiceProducerAnnotation.Port = sp.EndpointAnnotation.TargetPort;
}
else if (!sp.EndpointAnnotation.IsProxied)
{
diff --git a/src/Aspire.Hosting/Dcp/DcpVersion.cs b/src/Aspire.Hosting/Dcp/DcpVersion.cs
index 22e3d746983..a8290757aff 100644
--- a/src/Aspire.Hosting/Dcp/DcpVersion.cs
+++ b/src/Aspire.Hosting/Dcp/DcpVersion.cs
@@ -6,7 +6,7 @@ namespace Aspire.Hosting.Dcp;
internal static class DcpVersion
{
public static Version MinimumVersionInclusive = new Version(0, 1, 55);
- public static Version MinimumVersionIdeProtocolV1 = new Version(0, 1, 59);
+ public static Version MinimumVersionIdeProtocolV1 = new Version(0, 1, 61);
///
/// Development build version proxy, considered always "current" and supporting latest features.
diff --git a/src/Aspire.Hosting/Dcp/ResourceLogSource.cs b/src/Aspire.Hosting/Dcp/ResourceLogSource.cs
index be8af996856..c61ca894f28 100644
--- a/src/Aspire.Hosting/Dcp/ResourceLogSource.cs
+++ b/src/Aspire.Hosting/Dcp/ResourceLogSource.cs
@@ -39,12 +39,15 @@ public async IAsyncEnumerator GetAsyncEnumerator(CancellationToken
var stderrStreamTask = Task.Run(() => StreamLogsAsync(stderrStream, isError: true), cancellationToken);
// End the enumeration when both streams have been read to completion.
- _ = Task.WhenAll(stdoutStreamTask, stderrStreamTask).ContinueWith
- (_ => { channel.Writer.TryComplete(); },
- cancellationToken,
- TaskContinuationOptions.None,
- TaskScheduler.Default).ConfigureAwait(false);
+ async Task WaitForStreamsToCompleteAsync()
+ {
+ await Task.WhenAll(stdoutStreamTask, stderrStreamTask).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
+ channel.Writer.TryComplete();
+ }
+
+ _ = WaitForStreamsToCompleteAsync();
+
await foreach (var batch in channel.GetBatchesAsync(cancellationToken: cancellationToken))
{
yield return batch;
diff --git a/src/Aspire.Hosting/DistributedApplication.cs b/src/Aspire.Hosting/DistributedApplication.cs
index 5e98677c386..4554edb16f7 100644
--- a/src/Aspire.Hosting/DistributedApplication.cs
+++ b/src/Aspire.Hosting/DistributedApplication.cs
@@ -110,7 +110,8 @@ public void Run()
RunAsync().Wait();
}
- private async Task ExecuteBeforeStartHooksAsync(CancellationToken cancellationToken)
+ // Internal for testing
+ internal async Task ExecuteBeforeStartHooksAsync(CancellationToken cancellationToken)
{
AspireEventSource.Instance.AppBeforeStartHooksStart();
diff --git a/src/Aspire.Hosting/Exceptions.cs b/src/Aspire.Hosting/Exceptions.cs
new file mode 100644
index 00000000000..01311f38915
--- /dev/null
+++ b/src/Aspire.Hosting/Exceptions.cs
@@ -0,0 +1,11 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Aspire.Hosting;
+
+internal class FailedToApplyEnvironmentException : DistributedApplicationException
+{
+ public FailedToApplyEnvironmentException() { }
+ public FailedToApplyEnvironmentException(string message) : base(message) { }
+ public FailedToApplyEnvironmentException(string message, Exception inner) : base(message, inner) { }
+}
diff --git a/src/Aspire.Hosting/ExecutableResourceBuilderExtensions.cs b/src/Aspire.Hosting/ExecutableResourceBuilderExtensions.cs
index a8d84bacc90..b273a9b1b9e 100644
--- a/src/Aspire.Hosting/ExecutableResourceBuilderExtensions.cs
+++ b/src/Aspire.Hosting/ExecutableResourceBuilderExtensions.cs
@@ -36,18 +36,6 @@ public static IResourceBuilder AddExecutable(this IDistribut
});
}
- ///
- /// Adds annotation to to support containerization during deployment.
- ///
- /// Type of executable resource
- /// Resource builder
- /// A reference to the .
- [Obsolete("Use PublishAsDockerFile instead")]
- public static IResourceBuilder AsDockerfileInManifest(this IResourceBuilder builder) where T : ExecutableResource
- {
- return builder.PublishAsDockerFile();
- }
-
///
/// Adds annotation to to support containerization during deployment.
/// The resulting container image is built, and when the optional are provided
@@ -79,6 +67,6 @@ private static async Task WriteExecutableAsDockerfileResourceAsync(ManifestPubli
}
await context.WriteEnvironmentVariablesAsync(executable).ConfigureAwait(false);
- context.WriteBindings(executable, emitContainerPort: true);
+ context.WriteBindings(executable);
}
}
diff --git a/src/Aspire.Hosting/ProjectResourceBuilderExtensions.cs b/src/Aspire.Hosting/ProjectResourceBuilderExtensions.cs
index 605aac5924e..7e35a57c9f1 100644
--- a/src/Aspire.Hosting/ProjectResourceBuilderExtensions.cs
+++ b/src/Aspire.Hosting/ProjectResourceBuilderExtensions.cs
@@ -200,29 +200,6 @@ public static IResourceBuilder WithReplicas(this IResourceBu
return builder;
}
- ///
- /// Configures which launch profile should be used when running the project.
- ///
- /// The project resource builder.
- /// The name of the launch profile to use for execution.
- /// A reference to the .
- [Obsolete("This API is replaced by the AddProject overload that accepts a launchProfileName. Method will be removed by GA.")]
- public static IResourceBuilder WithLaunchProfile(this IResourceBuilder builder, string launchProfileName)
- {
- throw new InvalidOperationException("This API is replaced by the AddProject overload that accepts a launchProfileName. Method will be removed by GA.");
- }
-
- ///
- /// Configures the project to exclude launch profile settings when running.
- ///
- /// The project resource builder.
- /// A reference to the .
- [Obsolete("This API is replaced by the AddProject overload that accepts a launchProfileName. Null means exclude launch profile. Method will be removed by GA.")]
- public static IResourceBuilder ExcludeLaunchProfile(this IResourceBuilder builder)
- {
- throw new InvalidOperationException("This API is replaced by the AddProject overload that accepts a launchProfileName. Null means exclude launch profile. Method will be removed by GA.");
- }
-
///
/// Configures the project to disable forwarded headers when being published.
///
diff --git a/src/Aspire.Hosting/Properties/xlf/Resources.cs.xlf b/src/Aspire.Hosting/Properties/xlf/Resources.cs.xlf
index c6621b080b6..35a4d1adcec 100644
--- a/src/Aspire.Hosting/Properties/xlf/Resources.cs.xlf
+++ b/src/Aspire.Hosting/Properties/xlf/Resources.cs.xlf
@@ -15,7 +15,8 @@
- Nepovedlo se najít modul runtime {0} kontejneru. Chyba z kontroly modulu runtime kontejneru byla: {1}.
+ Nepovedlo se najít modul runtime {0} kontejneru. Chyba z kontroly modulu runtime kontejneru byla: {1}.
+Další podrobnosti o podporovaných modulech runtime kontejnerů najdete na https://aka.ms/dotnet/aspire/containers.
diff --git a/src/Aspire.Hosting/Properties/xlf/Resources.de.xlf b/src/Aspire.Hosting/Properties/xlf/Resources.de.xlf
index 44d41109473..9c3060269fe 100644
--- a/src/Aspire.Hosting/Properties/xlf/Resources.de.xlf
+++ b/src/Aspire.Hosting/Properties/xlf/Resources.de.xlf
@@ -4,18 +4,19 @@
- Anonymous volumes cannot be read-only.
+ Anonyme Volumes können nicht schreibgeschützt sein.
- Bind mounts must specify a source path.
+ Für "Bereitstellungen einbinden" muss ein Quellpfad angegeben werden.
- Die Containerruntime "{0}" wurde nicht gefunden. Fehler bei der Überprüfung der Containerruntime: {1}.
+ Die Containerruntime „{0}“ wurde nicht gefunden. Fehler bei der Überprüfung der Containerruntime: {1}.
+Weitere Informationen zu unterstützten Containerruntimes finden Sie unter „https://aka.ms/dotnet/aspire/containers“.
diff --git a/src/Aspire.Hosting/Properties/xlf/Resources.es.xlf b/src/Aspire.Hosting/Properties/xlf/Resources.es.xlf
index 18894ea0b8e..86ba615f300 100644
--- a/src/Aspire.Hosting/Properties/xlf/Resources.es.xlf
+++ b/src/Aspire.Hosting/Properties/xlf/Resources.es.xlf
@@ -15,7 +15,8 @@
- No se encontró el runtime del contenedor ''{0}''. El error de la comprobación del runtime del contenedor fue: {1}.
+ No se encontró el runtime del contenedor ''{0}''. El error de la comprobación del runtime del contenedor fue: {1}.
+Consulte https://aka.ms/dotnet/aspire/containers para obtener más información sobre los runtimes de contenedor admitidos.
diff --git a/src/Aspire.Hosting/Properties/xlf/Resources.fr.xlf b/src/Aspire.Hosting/Properties/xlf/Resources.fr.xlf
index 5991a148a73..7a25bacf1d9 100644
--- a/src/Aspire.Hosting/Properties/xlf/Resources.fr.xlf
+++ b/src/Aspire.Hosting/Properties/xlf/Resources.fr.xlf
@@ -15,7 +15,9 @@
- Le runtime du conteneur '{0}' est introuvable. L’erreur à partir de la vérification du runtime du conteneur était : {1}.
+ Le runtime du conteneur « {0} » est introuvable. L’erreur à partir de la vérification du runtime du conteneur était : {1}.
+.
+Pour plus d’informations sur les runtimes de conteneur pris en charge, consultez https://aka.ms/dotnet/aspire/containers.
diff --git a/src/Aspire.Hosting/Properties/xlf/Resources.it.xlf b/src/Aspire.Hosting/Properties/xlf/Resources.it.xlf
index 0b6224286e9..28486167c48 100644
--- a/src/Aspire.Hosting/Properties/xlf/Resources.it.xlf
+++ b/src/Aspire.Hosting/Properties/xlf/Resources.it.xlf
@@ -15,7 +15,8 @@
- Non è stato possibile trovare il runtime del contenitore "{0}". Errore del controllo del runtime del contenitore: {1}.
+ Non è stato possibile trovare il runtime del contenitore "{0}". Errore del controllo del runtime del contenitore: {1}.
+Vedere https://aka.ms/dotnet/aspire/containers per altri dettagli sui runtime del contenitore supportati.
diff --git a/src/Aspire.Hosting/Properties/xlf/Resources.ja.xlf b/src/Aspire.Hosting/Properties/xlf/Resources.ja.xlf
index af3b3d6bcf7..8995307da20 100644
--- a/src/Aspire.Hosting/Properties/xlf/Resources.ja.xlf
+++ b/src/Aspire.Hosting/Properties/xlf/Resources.ja.xlf
@@ -15,7 +15,8 @@
- コンテナー ランタイム '{0}' が見つかりませんでした。コンテナー ランタイム チェックのエラー: {1}。
+ コンテナー ランタイム '{0}' が見つかりませんでした。コンテナー ランタイム チェックのエラー: {1}.
+サポートされているコンテナー ランタイムの詳細については、https://aka.ms/dotnet/aspire/containers を参照してください。
diff --git a/src/Aspire.Hosting/Properties/xlf/Resources.ko.xlf b/src/Aspire.Hosting/Properties/xlf/Resources.ko.xlf
index d0868a31a41..b6d2a1b2b30 100644
--- a/src/Aspire.Hosting/Properties/xlf/Resources.ko.xlf
+++ b/src/Aspire.Hosting/Properties/xlf/Resources.ko.xlf
@@ -15,7 +15,8 @@
- 컨테이너 런타임 '{0}'(을)를 찾을 수 없습니다. 컨테이너 런타임 검사에서 오류가 발생했습니다. {1}.
+ '{0}' 컨테이너 런타임을 찾을 수 없습니다. 컨테이너 런타임 검사의 오류는 {1}이었습니다.
+지원되는 컨테이너 런타임에 관한 자세한 내용은 https://aka.ms/dotnet/aspire/containers를 참조하세요.
diff --git a/src/Aspire.Hosting/Properties/xlf/Resources.pl.xlf b/src/Aspire.Hosting/Properties/xlf/Resources.pl.xlf
index aafc3b67767..6cd3bf29670 100644
--- a/src/Aspire.Hosting/Properties/xlf/Resources.pl.xlf
+++ b/src/Aspire.Hosting/Properties/xlf/Resources.pl.xlf
@@ -15,7 +15,8 @@
- Nie można odnaleźć środowiska uruchomieniowego kontenera „{0}”. Błąd sprawdzania środowiska uruchomieniowego kontenera: {1}.
+ Nie można odnaleźć środowiska uruchomieniowego kontenera „{0}”. Błąd podczas sprawdzania środowiska uruchomieniowego kontenera: {1}.
+Aby uzyskać więcej informacji o obsługiwanych środowiskach uruchomieniowych kontenerów, zobacz https://aka.ms/dotnet/aspire/containers.
diff --git a/src/Aspire.Hosting/Properties/xlf/Resources.pt-BR.xlf b/src/Aspire.Hosting/Properties/xlf/Resources.pt-BR.xlf
index 42f9e982fd8..25a6027b9e6 100644
--- a/src/Aspire.Hosting/Properties/xlf/Resources.pt-BR.xlf
+++ b/src/Aspire.Hosting/Properties/xlf/Resources.pt-BR.xlf
@@ -15,7 +15,8 @@
- O runtime do contêiner '{0}' não pôde ser encontrado. O erro da verificação de runtime do contêiner foi: {1}.
+ O runtime do contêiner "{0}" não pôde ser encontrado. O erro da verificação de runtime do contêiner foi : {1}.
+Consulte https://aka.ms/dotnet/aspire/containers para obter mais detalhes sobre runtimes de contêiner com suporte.
diff --git a/src/Aspire.Hosting/Properties/xlf/Resources.ru.xlf b/src/Aspire.Hosting/Properties/xlf/Resources.ru.xlf
index 3a39fd68792..029c8a5c4dd 100644
--- a/src/Aspire.Hosting/Properties/xlf/Resources.ru.xlf
+++ b/src/Aspire.Hosting/Properties/xlf/Resources.ru.xlf
@@ -15,7 +15,8 @@
- Не удалось найти среду выполнения контейнера "{0}". Ошибка, выданная при проверке среды выполнения контейнера: {1}.
+ Не удалось найти среду выполнения контейнера "{0}". Ошибка проверки среды выполнения контейнера: {1}.
+Дополнительные сведения о поддерживаемых средах выполнения контейнеров см. на странице https://aka.ms/dotnet/aspire/containers.
diff --git a/src/Aspire.Hosting/Properties/xlf/Resources.tr.xlf b/src/Aspire.Hosting/Properties/xlf/Resources.tr.xlf
index 419599aee13..2309b067887 100644
--- a/src/Aspire.Hosting/Properties/xlf/Resources.tr.xlf
+++ b/src/Aspire.Hosting/Properties/xlf/Resources.tr.xlf
@@ -15,7 +15,9 @@
- '{0}' kapsayıcı çalışma zamanı bulunamadı. Kapsayıcı çalışma zamanı denetimi hatası: {1}.
+ '{0}' kapsayıcı çalışma zamanı bulunamadı. Kapsayıcı çalışma zamanı denetiminden gelen hata: {1}.
+.
+Desteklenen konteyner çalışma zamanları hakkında daha fazla ayrıntı için https://aka.ms/dotnet/aspire/containers adresine bakın.
diff --git a/src/Aspire.Hosting/Properties/xlf/Resources.zh-Hans.xlf b/src/Aspire.Hosting/Properties/xlf/Resources.zh-Hans.xlf
index 34ea44b283e..985deee9b8a 100644
--- a/src/Aspire.Hosting/Properties/xlf/Resources.zh-Hans.xlf
+++ b/src/Aspire.Hosting/Properties/xlf/Resources.zh-Hans.xlf
@@ -15,7 +15,8 @@
- 找不到容器运行时“{0}”。容器运行时检查中的错误为: {1}。
+ 找不到容器运行时“{0}”。容器运行时检查中的错误为: {1}。
+有关受支持的容器运行时的更多详细信息,请参阅 https://aka.ms/dotnet/aspire/containers。
diff --git a/src/Aspire.Hosting/Properties/xlf/Resources.zh-Hant.xlf b/src/Aspire.Hosting/Properties/xlf/Resources.zh-Hant.xlf
index 6d4f3337b63..3f19ff876de 100644
--- a/src/Aspire.Hosting/Properties/xlf/Resources.zh-Hant.xlf
+++ b/src/Aspire.Hosting/Properties/xlf/Resources.zh-Hant.xlf
@@ -15,7 +15,8 @@
- 找不到容器執行階段 '{0}'。容器執行階段檢查的錯誤為: {1}。
+ 找不到容器執行階段 '{0}'。來自容器執行階段檢查的錯誤為: {1}。
+如需支援的容器執行階段的詳細資訊,請參閱 https://aka.ms/dotnet/aspire/containers。
diff --git a/src/Aspire.Hosting/Publishing/ManifestPublishingContext.cs b/src/Aspire.Hosting/Publishing/ManifestPublishingContext.cs
index bc0ea47bc7b..feefc158219 100644
--- a/src/Aspire.Hosting/Publishing/ManifestPublishingContext.cs
+++ b/src/Aspire.Hosting/Publishing/ManifestPublishingContext.cs
@@ -31,6 +31,8 @@ public sealed class ManifestPublishingContext(DistributedApplicationExecutionCon
///
public Utf8JsonWriter Writer { get; } = writer;
+ private PortAllocator PortAllocator { get; } = new();
+
///
/// Gets cancellation token for this operation.
///
@@ -242,7 +244,7 @@ public async Task WriteContainerAsync(ContainerResource container)
WriteContainerMounts(container);
await WriteEnvironmentVariablesAsync(container).ConfigureAwait(false);
- WriteBindings(container, emitContainerPort: true);
+ WriteBindings(container);
}
///
@@ -264,8 +266,7 @@ public void WriteConnectionString(IResource resource)
/// collection.
///
/// The that contains annotations.
- /// Flag to determine whether container port is emitted.
- public void WriteBindings(IResource resource, bool emitContainerPort = false)
+ public void WriteBindings(IResource resource)
{
if (resource.TryGetEndpoints(out var endpoints))
{
@@ -277,9 +278,49 @@ public void WriteBindings(IResource resource, bool emitContainerPort = false)
Writer.WriteString("protocol", endpoint.Protocol.ToString().ToLowerInvariant());
Writer.WriteString("transport", endpoint.Transport);
- if (emitContainerPort && endpoint.ContainerPort is { } containerPort)
+ int? targetPort = (resource, endpoint.UriScheme, endpoint.TargetPort) switch
+ {
+ // The port was specified so use it
+ (_, _, int port) => port,
+
+ // Project resources get their default port from the deployment tool
+ // ideally we would default to a known port but we don't know it at this point
+ (ProjectResource, var scheme, null) when scheme is "http" or "https" => null,
+
+ // Allocate a dynamic port
+ _ => PortAllocator.AllocatePort()
+ };
+
+ int? exposedPort = (endpoint.UriScheme, endpoint.Port, targetPort) switch
+ {
+ // Exposed port and target port are the same, we don't need to mention the exposed port
+ (_, int p0, int p1) when p0 == p1 => null,
+
+ // Port was specified, so use it
+ (_, int port, _) => port,
+
+ // We have a target port, not need to specify an exposedPort
+ // it will default to the targetPort
+ (_, null, int port) => null,
+
+ // Let the tool infer the default http and https ports
+ ("http", null, null) => null,
+ ("https", null, null) => null,
+
+ // Other schemes just allocate a port
+ _ => PortAllocator.AllocatePort()
+ };
+
+ if (exposedPort is int ep)
+ {
+ PortAllocator.AddUsedPort(ep);
+ Writer.WriteNumber("port", ep);
+ }
+
+ if (targetPort is int tp)
{
- Writer.WriteNumber("containerPort", containerPort);
+ PortAllocator.AddUsedPort(tp);
+ Writer.WriteNumber("targetPort", tp);
}
if (endpoint.IsExternal)
@@ -387,7 +428,7 @@ public void WritePortBindingEnvironmentVariables(IResource resource)
continue;
}
- Writer.WriteString(endpoint.EnvironmentVariable, $"{{{resource.Name}.bindings.{endpoint.Name}.port}}");
+ Writer.WriteString(endpoint.EnvironmentVariable, $"{{{resource.Name}.bindings.{endpoint.Name}.targetPort}}");
}
}
}
diff --git a/src/Aspire.Hosting/Publishing/PortAllocator.cs b/src/Aspire.Hosting/Publishing/PortAllocator.cs
new file mode 100644
index 00000000000..40f36fbebbd
--- /dev/null
+++ b/src/Aspire.Hosting/Publishing/PortAllocator.cs
@@ -0,0 +1,29 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Aspire.Hosting.Publishing;
+
+// Used for the manifest publisher to dynamically allocate ports
+internal sealed class PortAllocator(int startPort = 8000)
+{
+ private int _allocatedPortStart = startPort;
+ private readonly HashSet _usedPorts = [];
+
+ public int AllocatePort()
+ {
+ while (true)
+ {
+ if (!_usedPorts.Contains(_allocatedPortStart))
+ {
+ return _allocatedPortStart;
+ }
+
+ _allocatedPortStart++;
+ }
+ }
+
+ public void AddUsedPort(int port)
+ {
+ _usedPorts.Add(port);
+ }
+}
diff --git a/src/Aspire.Hosting/ResourceBuilderExtensions.cs b/src/Aspire.Hosting/ResourceBuilderExtensions.cs
index d184a26349d..ee0ab824d36 100644
--- a/src/Aspire.Hosting/ResourceBuilderExtensions.cs
+++ b/src/Aspire.Hosting/ResourceBuilderExtensions.cs
@@ -115,6 +115,26 @@ public static IResourceBuilder WithEnvironment(this IResourceBuilder bu
});
}
+ ///
+ /// Adds an environment variable to the resource with the connection string from the referenced resource.
+ ///
+ /// The destination resource type.
+ /// The destination resource builder to which the environment variable will be added.
+ /// The name of the environment variable under which the connection string will be set.
+ /// The resource builder of the referenced service from which to pull the connection string.
+ /// The .
+ public static IResourceBuilder WithEnvironment(
+ this IResourceBuilder builder,
+ string envVarName,
+ IResourceBuilder resource)
+ where T : IResourceWithEnvironment
+ {
+ return builder.WithEnvironment(context =>
+ {
+ context.EnvironmentVariables[envVarName] = new ConnectionStringReference(resource.Resource, optional: false);
+ });
+ }
+
///
/// Adds the arguments to be passed to a container resource when the container is started.
///
@@ -335,30 +355,6 @@ private static void ApplyEndpoints(this IResourceBuilder builder, IResourc
}
}
- ///
- /// Exposes an endpoint on a resource. This endpoint reference can be retrieved using .
- /// The endpoint name will be the scheme name if not specified.
- ///
- /// The resource type.
- /// The resource builder.
- /// An optional host port.
- /// An optional scheme e.g. (http/https). Defaults to "tcp" if not specified.
- /// An optional name of the endpoint. Defaults to the scheme name if not specified.
- /// An optional name of the environment variable to inject.
- /// Specifies if the endpoint will be proxied by DCP. Defaults to true.
- /// The .
- /// Throws an exception if an endpoint with the same name already exists on the specified resource.
- public static IResourceBuilder WithEndpoint(this IResourceBuilder builder, int? hostPort = null, string? scheme = null, string? name = null, string? env = null, bool isProxied = true) where T : IResource
- {
- if (builder.Resource.Annotations.OfType().Any(sb => string.Equals(sb.Name, name, StringComparisons.EndpointAnnotationName)))
- {
- throw new DistributedApplicationException($"Endpoint '{name}' already exists. Endpoint names are case-insensitive.");
- }
-
- var annotation = new EndpointAnnotation(ProtocolType.Tcp, uriScheme: scheme, name: name, port: hostPort, env: env, isProxied: isProxied);
- return builder.WithAnnotation(annotation);
- }
-
///
/// Changes an existing creates a new endpoint if it doesn't exist and invokes callback to modify the defaults.
///
@@ -393,53 +389,21 @@ public static IResourceBuilder WithEndpoint(this IResourceBuilder build
return builder;
}
- ///
- /// Exposes an HTTP endpoint on a resource. This endpoint reference can be retrieved using .
- /// The endpoint name will be "http" if not specified.
- ///
- /// The resource type.
- /// The resource builder.
- /// An optional host port.
- /// An optional name of the endpoint. Defaults to "http" if not specified.
- /// An optional name of the environment variable to inject.
- /// The .
- /// Throws an exception if an endpoint with the same name already exists on the specified resource.
- public static IResourceBuilder WithHttpEndpoint(this IResourceBuilder builder, int? hostPort = null, string? name = null, string? env = null) where T : IResource
- {
- return builder.WithEndpoint(hostPort: hostPort, scheme: "http", name: name, env: env);
- }
-
- ///
- /// Exposes an HTTPS endpoint on a resource. This endpoint reference can be retrieved using .
- /// The endpoint name will be "https" if not specified.
- ///
- /// The resource type.
- /// The resource builder.
- /// An optional host port.
- /// An optional name of the endpoint. Defaults to "https" if not specified.
- /// An optional name of the environment variable to inject.
- /// The .
- /// Throws an exception if an endpoint with the same name already exists on the specified resource.
- public static IResourceBuilder WithHttpsEndpoint(this IResourceBuilder builder, int? hostPort = null, string? name = null, string? env = null) where T : IResource
- {
- return builder.WithEndpoint(hostPort: hostPort, scheme: "https", name: name, env: env);
- }
-
///
/// Exposes an endpoint on a resource. This endpoint reference can be retrieved using .
/// The endpoint name will be the scheme name if not specified.
///
/// The resource type.
/// The resource builder.
- /// The container port.
- /// An optional host port.
+ /// This is the port the resource is listening on. If the endpoint is used for the container, it is the container port.
+ /// An optional port. This is the port that will be given to other resource to communicate with this resource.
/// An optional scheme e.g. (http/https). Defaults to "tcp" if not specified.
/// An optional name of the endpoint. Defaults to the scheme name if not specified.
/// An optional name of the environment variable to inject.
/// Specifies if the endpoint will be proxied by DCP. Defaults to true.
/// The .
/// Throws an exception if an endpoint with the same name already exists on the specified resource.
- public static IResourceBuilder WithEndpoint(this IResourceBuilder builder, int containerPort, int? hostPort = null, string? scheme = null, string? name = null, string? env = null, bool isProxied = true) where T : IResource
+ public static IResourceBuilder WithEndpoint(this IResourceBuilder builder, int? port = null, int? targetPort = null, string? scheme = null, string? name = null, string? env = null, bool isProxied = true) where T : IResource
{
if (builder.Resource.Annotations.OfType().Any(sb => string.Equals(sb.Name, name, StringComparisons.EndpointAnnotationName)))
{
@@ -450,8 +414,8 @@ public static IResourceBuilder WithEndpoint(this IResourceBuilder build
protocol: ProtocolType.Tcp,
uriScheme: scheme,
name: name,
- port: hostPort,
- containerPort: containerPort,
+ port: port,
+ targetPort: targetPort,
env: env,
isProxied: isProxied);
@@ -464,16 +428,16 @@ public static IResourceBuilder WithEndpoint(this IResourceBuilder build
///
/// The resource type.
/// The resource builder.
- /// The container port.
- /// An optional host port.
+ /// This is the port the resource is listening on. If the endpoint is used for the container, it is the container port.
+ /// An optional port. This is the port that will be given to other resource to communicate with this resource.
/// An optional name of the endpoint. Defaults to "http" if not specified.
/// An optional name of the environment variable to inject.
/// Specifies if the endpoint will be proxied by DCP. Defaults to true.
/// The .
/// Throws an exception if an endpoint with the same name already exists on the specified resource.
- public static IResourceBuilder WithHttpEndpoint(this IResourceBuilder builder, int containerPort, int? hostPort = null, string? name = null, string? env = null, bool isProxied = true) where T : IResource
+ public static IResourceBuilder WithHttpEndpoint(this IResourceBuilder builder, int? port = null, int? targetPort = null, string? name = null, string? env = null, bool isProxied = true) where T : IResource
{
- return builder.WithEndpoint(containerPort: containerPort, hostPort: hostPort, scheme: "http", name: name, env: env, isProxied: isProxied);
+ return builder.WithEndpoint(targetPort: targetPort, port: port, scheme: "http", name: name, env: env, isProxied: isProxied);
}
///
@@ -482,20 +446,44 @@ public static IResourceBuilder WithHttpEndpoint(this IResourceBuilder b
///
/// The resource type.
/// The resource builder.
- /// The container port.
- /// An optional host port.
+ /// This is the port the resource is listening on. If the endpoint is used for the container, it is the container port.
+ /// An optional host port.
/// An optional name of the endpoint. Defaults to "https" if not specified.
/// An optional name of the environment variable to inject.
/// Specifies if the endpoint will be proxied by DCP. Defaults to true.
/// The .
/// Throws an exception if an endpoint with the same name already exists on the specified resource.
- public static IResourceBuilder WithHttpsEndpoint(this IResourceBuilder builder, int containerPort, int? hostPort = null, string? name = null, string? env = null, bool isProxied = true) where T : IResource
+ public static IResourceBuilder WithHttpsEndpoint(this IResourceBuilder builder, int? port = null, int? targetPort = null, string? name = null, string? env = null, bool isProxied = true) where T : IResource
{
- return builder.WithEndpoint(containerPort: containerPort, hostPort: hostPort, scheme: "https", name: name, env: env, isProxied: isProxied);
+ return builder.WithEndpoint(targetPort: targetPort, port: port, scheme: "https", name: name, env: env, isProxied: isProxied);
+ }
+
+ ///
+ /// Marks existing http or https endpoints on a resource as external.
+ ///
+ /// The resource type.
+ /// The resource builder.
+ ///
+ public static IResourceBuilder WithExternalHttpEndpoints(this IResourceBuilder builder) where T : IResourceWithEndpoints
+ {
+ if (!builder.Resource.TryGetAnnotationsOfType(out var endpoints))
+ {
+ return builder;
+ }
+
+ foreach (var endpoint in endpoints)
+ {
+ if (endpoint.UriScheme == "http" || endpoint.UriScheme == "https")
+ {
+ endpoint.IsExternal = true;
+ }
+ }
+
+ return builder;
}
///
- /// Gets an by name from the resource. These endpoints are declared either using or by launch settings (for project resources).
+ /// Gets an by name from the resource. These endpoints are declared either using or by launch settings (for project resources).
/// The can be used to resolve the address of the endpoint in .
///
/// The resource type.
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.cs.json b/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.cs.json
index fc2ab469c34..1620d71d24c 100644
--- a/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.cs.json
+++ b/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.cs.json
@@ -4,8 +4,8 @@
"description": "Šablona projektu pro vytvoření aplikace .NET Aspire s webovým front-endem Blazor a back-endovou službou webového rozhraní API, volitelně s využitím Redisu pro ukládání do mezipaměti.",
"symbols/Framework/description": "Cílová architektura pro projekt",
"symbols/Framework/choices/net8.0/description": "Cílový net8.0",
- "symbols/UseRedisCache/displayName": "_Use Redis pro ukládání do mezipaměti (vyžaduje Docker)",
- "symbols/UseRedisCache/description": "Nakonfiguruje, jestli se má aplikace nastavit tak, aby pro ukládání do mezipaměti používala Redis. Vyžaduje místní spuštění Dockeru.",
+ "symbols/UseRedisCache/displayName": "_Použít Redis pro ukládání do mezipaměti (vyžaduje podporovaný modul runtime kontejneru)",
+ "symbols/UseRedisCache/description": "Nakonfiguruje, jestli se má aplikace nastavit tak, aby pro ukládání do mezipaměti používala Redis. K místnímu spouštění se vyžaduje podporovaný modul runtime kontejneru. Další podrobnosti najdete na https://aka.ms/dotnet/aspire/containers.",
"symbols/CreateTestsProject/displayName": "Vytvoření projektu _tests",
"symbols/CreateTestsProject/description": "Konfiguruje, jestli se má vytvořit projekt pro testy integrace pomocí projektu AppHost.",
"symbols/appHostHttpPort/description": "Číslo portu, který se má použít pro koncový bod HTTP v launchSettings.json projektu AppHost.",
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.de.json b/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.de.json
index 53b69290952..2f4e6db84d9 100644
--- a/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.de.json
+++ b/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.de.json
@@ -4,8 +4,8 @@
"description": "Eine Projektvorlage zum Erstellen einer .NET Aspire-App mit einem Blazor-Web-Front-End und einem Web-API-Back-End-Dienst, die optional Redis zum Zwischenspeichern verwendet.",
"symbols/Framework/description": "Das Zielframework für das Projekt.",
"symbols/Framework/choices/net8.0/description": "Ziel net8.0",
- "symbols/UseRedisCache/displayName": "_Use Redis zum Zwischenspeichern (erfordert Docker)",
- "symbols/UseRedisCache/description": "Konfiguriert, ob die Anwendung für die Verwendung von Redis für die Zwischenspeicherung eingerichtet werden soll. Erfordert, dass Docker lokal ausgeführt wird.",
+ "symbols/UseRedisCache/displayName": "_Use Redis für die Zwischenspeicherung (erfordert eine unterstützte Container-Runtime)",
+ "symbols/UseRedisCache/description": "Konfiguriert, ob die Anwendung für die Verwendung von Redis für die Zwischenspeicherung eingerichtet werden soll. Erfordert eine unterstützte Container-Runtime, um lokal ausgeführt zu werden. Weitere Informationen finden Sie unter https://aka.ms/dotnet/aspire/containers.",
"symbols/CreateTestsProject/displayName": "Erstellen eines _tests-Projekts",
"symbols/CreateTestsProject/description": "Konfiguriert, ob ein Projekt für Integrationstests mithilfe des AppHost-Projekts erstellt werden soll.",
"symbols/appHostHttpPort/description": "Portnummer, die für den HTTP-Endpunkt in launchSettings.json des AppHost-Projekts verwendet werden soll.",
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.es.json b/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.es.json
index b7b83c27d03..e6f7f5588ff 100644
--- a/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.es.json
+++ b/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.es.json
@@ -4,8 +4,8 @@
"description": "Una plantilla de proyecto para crear una aplicación de .NET Insights con un front-end web de Blazor y un servicio back-end de API web, opcionalmente mediante Redis para el almacenamiento en caché.",
"symbols/Framework/description": "Marco de destino del proyecto.",
"symbols/Framework/choices/net8.0/description": "NET8.0 de destino",
- "symbols/UseRedisCache/displayName": "_Use Redis para el almacenamiento en caché (requiere Docker)",
- "symbols/UseRedisCache/description": "Configura si se va a configurar la aplicación para que use Redis para el almacenamiento en caché. Requiere que Docker se ejecute localmente.",
+ "symbols/UseRedisCache/displayName": "_Use Redis para el almacenamiento en caché (requiere un runtime de contenedor compatible)",
+ "symbols/UseRedisCache/description": "Configura si se va a configurar la aplicación para que use Redis para el almacenamiento en caché. Requiere un contenedor de runtime compatible para ejecutarse localmente. Consulte https://aka.ms/dotnet/aspire/containers para obtener más detalles.",
"symbols/CreateTestsProject/displayName": "Creación de un proyecto _tests",
"symbols/CreateTestsProject/description": "Configura si se va a crear un proyecto para las pruebas de integración mediante el proyecto AppHost.",
"symbols/appHostHttpPort/description": "Número de puerto que se va a usar para el punto de conexión HTTP en launchSettings.json del proyecto AppHost.",
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.fr.json b/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.fr.json
index 3fb7bb33e14..cbfe80d4fba 100644
--- a/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.fr.json
+++ b/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.fr.json
@@ -4,8 +4,8 @@
"description": "Un modèle de projet pour créer une application .NET Aspire avec une interface Web Blazor et un service backend d'API Web, en utilisant éventuellement Redis pour la mise en cache.",
"symbols/Framework/description": "Framework cible du projet.",
"symbols/Framework/choices/net8.0/description": "Cible net8.0",
- "symbols/UseRedisCache/displayName": "_Utiliser Redis pour la mise en cache (nécessite Docker)",
- "symbols/UseRedisCache/description": "Configure s'il faut configurer l'application pour utiliser Redis pour la mise en cache. Nécessite que Docker s'exécute localement.",
+ "symbols/UseRedisCache/displayName": "_Use Redis pour la mise en cache (nécessite un runtime de conteneur pris en charge)",
+ "symbols/UseRedisCache/description": "Configure s’il faut configurer l’application pour utiliser Redis pour la mise en cache. Nécessite un conteneur runtime supporté pour fonctionner localement, voir https://aka.ms/dotnet/aspire/containers pour plus de détails.",
"symbols/CreateTestsProject/displayName": "Créer un _projet de test",
"symbols/CreateTestsProject/description": "Configure s’il faut créer un projet pour les tests d’intégration à l’aide du projet AppHost.",
"symbols/appHostHttpPort/description": "Numéro de port à utiliser pour le point de terminaison HTTP dans launchSettings.json du projet AppHost.",
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.it.json b/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.it.json
index ab4e9942295..7ec28b83d86 100644
--- a/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.it.json
+++ b/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.it.json
@@ -4,8 +4,8 @@
"description": "Modello di progetto per la creazione di un'app .NET Aspire con un front-end Web Blazor e un servizio back-end dell'API Web, facoltativamente usando Redis per la memorizzazione nella cache.",
"symbols/Framework/description": "Il framework di destinazione per il progetto.",
"symbols/Framework/choices/net8.0/description": "Destinazione net8.0",
- "symbols/UseRedisCache/displayName": "_Use Redis per la memorizzazione nella cache (richiede Docker)",
- "symbols/UseRedisCache/description": "Configura se impostare l'applicazione per l'utilizzo di Redis per la memorizzazione nella cache. Richiede l'esecuzione locale di Docker.",
+ "symbols/UseRedisCache/displayName": "_Usare Redis per la memorizzazione nella cache (richiede un runtime del contenitore supportato)",
+ "symbols/UseRedisCache/description": "Configura se impostare l'applicazione per l'utilizzo di Redis per la memorizzazione nella cache. Richiede l'esecuzione locale di un runtime del contenitore, vedere https://aka.ms/dotnet/aspire/containers for more details.",
"symbols/CreateTestsProject/displayName": "Creare un progetto di _test",
"symbols/CreateTestsProject/description": "Configura se creare un progetto per i test di integrazione usando il progetto AppHost.",
"symbols/appHostHttpPort/description": "Numero di porta da usare per l'endpoint HTTP in launchSettings.json. del progetto AppHost.",
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.ja.json b/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.ja.json
index e956489d8a3..02d382d8672 100644
--- a/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.ja.json
+++ b/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.ja.json
@@ -4,8 +4,8 @@
"description": "Blazor Web フロントエンドと Web API バックエンド サービスを使用して .NET Aspire アプリを作成するためのプロジェクト テンプレート。必要に応じて、Redis をキャッシュに使用します。",
"symbols/Framework/description": "プロジェクトのターゲット フレームワークです。",
"symbols/Framework/choices/net8.0/description": "ターゲット net8.0",
- "symbols/UseRedisCache/displayName": "キャッシュ用に Redis を使用する (Docker が必要)(_U)",
- "symbols/UseRedisCache/description": "Redis をキャッシュに使用するようにアプリケーションを設定するかどうかを構成します。Docker をローカルで実行する必要があります。",
+ "symbols/UseRedisCache/displayName": "キャッシュ用に Redis を使用する (サポートされているコンテナー ランタイムが必要) (_U)",
+ "symbols/UseRedisCache/description": "Redis をキャッシュに使用するようにアプリケーションを設定するかどうかを構成します。ローカルで実行するには、サポートされているコンテナー ランタイムが必要です。詳細については、https://aka.ms/dotnet/aspire/containers を参照してください。",
"symbols/CreateTestsProject/displayName": "テスト プロジェクトの作成(_T)",
"symbols/CreateTestsProject/description": "AppHost プロジェクトを使用して統合テスト用のプロジェクトを作成するかどうかを構成します。",
"symbols/appHostHttpPort/description": "AppHost プロジェクトの launchSettings.json の HTTP エンドポイントに使用するポート番号。",
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.ko.json b/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.ko.json
index ea245d67212..d67846c166f 100644
--- a/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.ko.json
+++ b/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.ko.json
@@ -4,8 +4,8 @@
"description": "필요에 따라 캐싱에 Redis를 사용하여 Blazor 웹 프런트 엔드와 웹 API 백 엔드 서비스로 .NET Aspire 앱을 만들기 위한 프로젝트 템플릿입니다.",
"symbols/Framework/description": "프로젝트에 대한 대상 프레임워크입니다.",
"symbols/Framework/choices/net8.0/description": "대상 net8.0",
- "symbols/UseRedisCache/displayName": "_캐싱에 Redis 사용(Docker가 필요함)",
- "symbols/UseRedisCache/description": "캐싱에 Redis를 사용하도록 응용 프로그램을 설정할지 여부를 구성합니다. 로컬에서 실행하려면 Docker가 필요합니다.",
+ "symbols/UseRedisCache/displayName": "캐싱용 _Use Redis(지원되는 컨테이너 런타임 필요)",
+ "symbols/UseRedisCache/description": "캐싱에 Redis를 사용하도록 응용 프로그램을 설정할지 여부를 구성합니다. 로컬로 실행하려면 지원되는 컨테이너 런타임이 필요합니다. 자세한 내용은 https://aka.ms/dotnet/aspire/containers를 참조하세요.",
"symbols/CreateTestsProject/displayName": "_tests 프로젝트 만들기",
"symbols/CreateTestsProject/description": "AppHost 프로젝트를 사용하여 통합 테스트용 프로젝트를 만들지 여부를 구성합니다.",
"symbols/appHostHttpPort/description": "AppHost 프로젝트의 launchSettings.json HTTP 엔드포인트에 사용할 포트 번호입니다.",
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.pl.json b/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.pl.json
index 04ddc69b2bd..00be6470ceb 100644
--- a/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.pl.json
+++ b/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.pl.json
@@ -4,8 +4,8 @@
"description": "Szablon projektu służący do tworzenia aplikacji Aspire platformy .NET z frontonem internetowym platformy Blazor i usługą zaplecza internetowego interfejsu API, opcjonalnie używając usługi Redis do buforowania.",
"symbols/Framework/description": "Platforma docelowa dla tego projektu.",
"symbols/Framework/choices/net8.0/description": "Docelowa platforma net8.0",
- "symbols/UseRedisCache/displayName": "_Użyj usługi Redis na potrzeby buforowania (wymaga platformy Docker)",
- "symbols/UseRedisCache/description": "Określa, czy skonfigurować aplikację do używania usługi Redis do buforowania. Wymaga, aby platforma Docker działała lokalnie.",
+ "symbols/UseRedisCache/displayName": "_Skorzystaj z magazynu danych Redis na potrzeby buforowania (wymaga obsługiwanego środowiska uruchomieniowego kontenera)",
+ "symbols/UseRedisCache/description": "Określa, czy konfigurować aplikację do korzystania z magazynu danych Redis na potrzeby buforowania. Wymaga obsługiwanego środowiska uruchomieniowego kontenera na potrzeby uruchomienia lokalnego. Aby uzyskać więcej informacji, zobacz https://aka.ms/dotnet/aspire/containers.",
"symbols/CreateTestsProject/displayName": "Tworzenie projektu _testowego",
"symbols/CreateTestsProject/description": "Konfiguruje, czy utworzyć projekt na potrzeby testów integracji przy użyciu projektu AppHost.",
"symbols/appHostHttpPort/description": "Numer portu do użycia dla punktu końcowego HTTP w pliku launchSettings.json projektu AppHost.",
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.pt-BR.json b/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.pt-BR.json
index 761fedc8aa0..4490f4b8b72 100644
--- a/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.pt-BR.json
+++ b/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.pt-BR.json
@@ -4,8 +4,8 @@
"description": "Um modelo de projeto para criar um aplicativo .NET Pool com um front-end da Web Blazor e um serviço de back-end da API Web, opcionalmente usando o Redis para cache.",
"symbols/Framework/description": "A estrutura de destino do projeto.",
"symbols/Framework/choices/net8.0/description": "Destino net8.0",
- "symbols/UseRedisCache/displayName": "_Use Redis para cache (requer o Docker)",
- "symbols/UseRedisCache/description": "Configura se o aplicativo deve ser configurado para usar o Redis para cache. Requer que o Docker seja executado localmente.",
+ "symbols/UseRedisCache/displayName": "_Usar o Redis para cache (requer um tempo de execução de contêiner compatível)",
+ "symbols/UseRedisCache/description": "Configura se o aplicativo deve ser configurado para usar o Redis para cache. Requer um runtime contêiner compatível para ser executado localmente; consulte https://aka.ms/dotnet/aspire/containers para obter mais detalhes.",
"symbols/CreateTestsProject/displayName": "Criar um projeto _tests",
"symbols/CreateTestsProject/description": "Configura se um projeto deve ser criado para testes de integração usando o projeto AppHost.",
"symbols/appHostHttpPort/description": "Número da porta a ser usado para o ponto de extremidade HTTP launchSettings.json do projeto AppHost.",
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.ru.json b/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.ru.json
index 1f8658f8e57..15e94977eae 100644
--- a/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.ru.json
+++ b/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.ru.json
@@ -4,8 +4,8 @@
"description": "Шаблон проекта для создания приложения .NET Aspire с веб-интерфейсом Blazor и внутренней службой веб-API, при необходимости с использованием Redis для кэширования.",
"symbols/Framework/description": "Целевая платформа для проекта.",
"symbols/Framework/choices/net8.0/description": "Целевая среда net8.0",
- "symbols/UseRedisCache/displayName": "_Use redis для кэширования (требуется Docker)",
- "symbols/UseRedisCache/description": "Определяет, следует ли настраивать приложение для использования Redis для кэширования. Требуется Docker для локального запуска.",
+ "symbols/UseRedisCache/displayName": "_Использовать Redis для кэширования (требуется поддерживаемая среда выполнения контейнера)",
+ "symbols/UseRedisCache/description": "Определяет, следует ли настраивать приложение с целью использования Redis для кэширования. Требуется локальный запуск поддерживаемой среды выполнения контейнеров. Дополнительные сведения см. на странице https://aka.ms/dotnet/aspire/containers.",
"symbols/CreateTestsProject/displayName": "Создать _tests project",
"symbols/CreateTestsProject/description": "Указывает, создавать ли проект для интеграционных тестов с использованием проекта AppHost.",
"symbols/appHostHttpPort/description": "Номер порта, который будет использоваться для конечной точки HTTP в файле launchSettings.json проекта AppHost.",
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.tr.json b/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.tr.json
index a6ac41da43e..92468e269a2 100644
--- a/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.tr.json
+++ b/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.tr.json
@@ -4,8 +4,8 @@
"description": "Blazor web ön ucu ve web API'si arka uç hizmetiyle ve önbelleğe alırken isteğe bağlı olarak Redis'i kullanarak bir .NET Çalışanı uygulaması oluşturmak için proje şablonu.",
"symbols/Framework/description": "Projenin hedef çerçevesi.",
"symbols/Framework/choices/net8.0/description": "Hedef net8.0",
- "symbols/UseRedisCache/displayName": "_Önbelleğe almak için Redis kullan (Docker gerektirir)",
- "symbols/UseRedisCache/description": "Uygulamanın önbelleğe alırken Redis'i kullanmak üzere ayarlanıp ayarlanmayacağını yapılandırır. Docker'ın yerel olarak çalıştırılması gerekir.",
+ "symbols/UseRedisCache/displayName": "Önbelleğe alma için Redis’i k_ullan (desteklenen bir kapsayıcı çalışma zamanı gerektirir)",
+ "symbols/UseRedisCache/description": "Uygulamanın önbelleğe alma için Redis’i kullanmak üzere ayarlanıp ayarlanmayacağını yapılandırır. Yerel olarak çalıştırmak için desteklenen bir kapsayıcılar çalışma zamanı gerektirir. Daha fazla ayrıntı için https://aka.ms/dotnet/aspire/containers sayfasına bakın.",
"symbols/CreateTestsProject/displayName": "Bir _tests projesi oluştur",
"symbols/CreateTestsProject/description": "AppHost projesini kullanarak tümleştirme testleri için bir proje oluşturulup oluşturulmayacağını yapılandırır.",
"symbols/appHostHttpPort/description": "AppHost projesinin HTTP uç noktası launchSettings.json bağlantı noktası numarası.",
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.zh-Hans.json b/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.zh-Hans.json
index a6b61ee1bc3..befdf277a35 100644
--- a/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.zh-Hans.json
+++ b/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.zh-Hans.json
@@ -4,8 +4,8 @@
"description": "一个用于使用 Blazor Web 前端和 Web API 后端服务创建 .NET Aspire 应用的项目模板,可以选择使用 Redis 进行缓存。",
"symbols/Framework/description": "项目的目标框架。",
"symbols/Framework/choices/net8.0/description": "目标 net8.0",
- "symbols/UseRedisCache/displayName": "使用 Redis 进行缓存(_U)(需要 Docker)",
- "symbols/UseRedisCache/description": "配置是否将应用程序设置为使用 Redis 进行缓存。需要 Docker 才能在本地运行。",
+ "symbols/UseRedisCache/displayName": "_Use Redis for caching (requires a supported container runtime)",
+ "symbols/UseRedisCache/description": "配置是否将应用程序设置为使用 Redis 进行缓存。需要支持的容器运行时才能在本地运行,有关详细信息,请参阅 https://aka.ms/dotnet/aspire/containers。",
"symbols/CreateTestsProject/displayName": "创建 _tests 项目",
"symbols/CreateTestsProject/description": "配置是否使用 AppHost 项目为集成测试创建项目。",
"symbols/appHostHttpPort/description": "该端口号将用于 AppHost 项目的 launchSettings.json 中的 HTTP 终结点。",
diff --git a/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.zh-Hant.json b/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.zh-Hant.json
index 0bd752bfa15..d80a8aceb1a 100644
--- a/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.zh-Hant.json
+++ b/src/Aspire.ProjectTemplates/templates/aspire-starter/.template.config/localize/templatestrings.zh-Hant.json
@@ -4,8 +4,8 @@
"description": "用於使用 Blazor Web 前端和 Web API 後端服務 (選擇性地使用 Redis 進行快取) 來建立 .NET Aspire 應用程式的專案範本。",
"symbols/Framework/description": "專案的目標 Framework。",
"symbols/Framework/choices/net8.0/description": "目標 net8.0",
- "symbols/UseRedisCache/displayName": "使用 Redis 進行快取(_U) (需要 Docker)",
- "symbols/UseRedisCache/description": "設定是否要將應用程式設為使用 Redis 進行快取。需要 Docker 在本機執行。",
+ "symbols/UseRedisCache/displayName": "使用 Redis 進行快取 (需要支援的容器執行階段)(_U)",
+ "symbols/UseRedisCache/description": "設定是否要將應用程式設為使用 Redis 進行快取。需要支援的容器執行階段,才能在本機執行,如需詳細資料,請參閱 https://aka.ms/dotnet/aspire/containers。",
"symbols/CreateTestsProject/displayName": "建立測試專案(_T)",
"symbols/CreateTestsProject/description": "設定是否要使用 AppHost 專案為整合測試建立專案。",
"symbols/appHostHttpPort/description": "要用於 AppHost 專案 launchSettings.json 中 HTTP 端點的連接埠號碼。",
diff --git a/src/Components/Aspire.Azure.AI.OpenAI/AspireAzureOpenAIExtensions.cs b/src/Components/Aspire.Azure.AI.OpenAI/AspireAzureOpenAIExtensions.cs
index de1d96fb67b..583c3572610 100644
--- a/src/Components/Aspire.Azure.AI.OpenAI/AspireAzureOpenAIExtensions.cs
+++ b/src/Components/Aspire.Azure.AI.OpenAI/AspireAzureOpenAIExtensions.cs
@@ -22,22 +22,6 @@ public static class AspireAzureOpenAIExtensions
{
private const string DefaultConfigSectionName = "Aspire:Azure:AI:OpenAI";
- ///
- /// Registers as a singleton in the services provided by the .
- ///
- /// The to read config from and add services to.
- /// A name used to retrieve the connection string from the ConnectionStrings configuration section.
- /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration.
- /// An optional method that can be used for customizing the .
- /// Reads the configuration from "Aspire.Azure.AI.OpenAI" section.
- [Obsolete($"This method is obsolete and will be removed in a future version. Use {nameof(AddAzureOpenAIClient)} instead.")]
- public static void AddAzureOpenAI(
- this IHostApplicationBuilder builder,
- string connectionName,
- Action? configureSettings = null,
- Action>? configureClientBuilder = null)
- => AddAzureOpenAIClient(builder, connectionName, configureSettings, configureClientBuilder);
-
///
/// Registers as a singleton in the services provided by the .
///
@@ -55,22 +39,6 @@ public static void AddAzureOpenAIClient(
new OpenAIComponent().AddClient(builder, DefaultConfigSectionName, configureSettings, configureClientBuilder, connectionName, serviceKey: null);
}
- ///
- /// Registers as a singleton for given in the services provided by the .
- ///
- /// The to read config from and add services to.
- /// The name of the component, which is used as the of the service and also to retrieve the connection string from the ConnectionStrings configuration section.
- /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration.
- /// An optional method that can be used for customizing the .
- /// Reads the configuration from "Aspire.Azure.AI.OpenAI:{name}" section.
- [Obsolete($"This method is obsolete and will be removed in a future version. Use {nameof(AddKeyedAzureOpenAIClient)} instead.")]
- public static void AddKeyedAzureOpenAI(
- this IHostApplicationBuilder builder,
- string name,
- Action? configureSettings = null,
- Action>? configureClientBuilder = null)
- => AddKeyedAzureOpenAIClient(builder, name, configureSettings, configureClientBuilder);
-
///
/// Registers as a singleton for given in the services provided by the .
///
diff --git a/src/Components/Aspire.Azure.AI.OpenAI/ConfigurationSchema.json b/src/Components/Aspire.Azure.AI.OpenAI/ConfigurationSchema.json
index e4226618be9..db7a4c84ef0 100644
--- a/src/Components/Aspire.Azure.AI.OpenAI/ConfigurationSchema.json
+++ b/src/Components/Aspire.Azure.AI.OpenAI/ConfigurationSchema.json
@@ -69,12 +69,12 @@
"properties": {
"Delay": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "The delay between retry attempts for a fixed approach or the delay\non which to base calculations for a backoff-based approach.\nIf the service provides a Retry-After response header, the next retry will be delayed by the duration specified by the header value."
},
"MaxDelay": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "The maximum permissible delay between retry attempts when the service does not provide a Retry-After response header.\nIf the service provides a Retry-After response header, the next retry will be delayed by the duration specified by the header value."
},
"MaxRetries": {
@@ -90,7 +90,7 @@
},
"NetworkTimeout": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "The timeout applied to an individual network operations."
}
},
diff --git a/src/Components/Aspire.Azure.Data.Tables/AspireTablesExtensions.cs b/src/Components/Aspire.Azure.Data.Tables/AspireTablesExtensions.cs
index 21628473eb8..eb6df2ab9c6 100644
--- a/src/Components/Aspire.Azure.Data.Tables/AspireTablesExtensions.cs
+++ b/src/Components/Aspire.Azure.Data.Tables/AspireTablesExtensions.cs
@@ -21,24 +21,6 @@ public static class AspireTablesExtensions
{
private const string DefaultConfigSectionName = "Aspire:Azure:Data:Tables";
- ///
- /// Registers as a singleton in the services provided by the .
- /// Enables retries, corresponding health check, logging and telemetry.
- ///
- /// The to read config from and add services to.
- /// A name used to retrieve the connection string from the ConnectionStrings configuration section.
- /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration.
- /// An optional method that can be used for customizing the .
- /// Reads the configuration from "Aspire:Azure:Data:Tables" section.
- /// Thrown when neither nor is provided.
- [Obsolete($"This method is obsolete and will be removed in a future version. Use {nameof(AddAzureTableClient)} instead.")]
- public static void AddAzureTableService(
- this IHostApplicationBuilder builder,
- string connectionName,
- Action? configureSettings = null,
- Action>? configureClientBuilder = null)
- => AddAzureTableClient(builder, connectionName, configureSettings, configureClientBuilder);
-
///
/// Registers as a singleton in the services provided by the .
/// Enables retries, corresponding health check, logging and telemetry.
@@ -58,24 +40,6 @@ public static void AddAzureTableClient(
new TableServiceComponent().AddClient(builder, DefaultConfigSectionName, configureSettings, configureClientBuilder, connectionName, serviceKey: null);
}
- ///
- /// Registers as a singleton for given in the services provided by the .
- /// Enables retries, corresponding health check, logging and telemetry.
- ///
- /// The to read config from and add services to.
- /// The name of the component, which is used as the of the service and also to retrieve the connection string from the ConnectionStrings configuration section.
- /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration.
- /// An optional method that can be used for customizing the .
- /// Reads the configuration from "Aspire:Azure:Data:Tables:{name}" section.
- /// Thrown when neither nor is provided.
- [Obsolete($"This method is obsolete and will be removed in a future version. Use {nameof(AddKeyedAzureTableClient)} instead.")]
- public static void AddKeyedAzureTableService(
- this IHostApplicationBuilder builder,
- string name,
- Action? configureSettings = null,
- Action>? configureClientBuilder = null)
- => AddKeyedAzureTableClient(builder, name, configureSettings, configureClientBuilder);
-
///
/// Registers as a singleton for given in the services provided by the .
/// Enables retries, corresponding health check, logging and telemetry.
diff --git a/src/Components/Aspire.Azure.Data.Tables/ConfigurationSchema.json b/src/Components/Aspire.Azure.Data.Tables/ConfigurationSchema.json
index 61ec8f5e20c..6ef93a9599b 100644
--- a/src/Components/Aspire.Azure.Data.Tables/ConfigurationSchema.json
+++ b/src/Components/Aspire.Azure.Data.Tables/ConfigurationSchema.json
@@ -73,12 +73,12 @@
"properties": {
"Delay": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "The delay between retry attempts for a fixed approach or the delay\non which to base calculations for a backoff-based approach.\nIf the service provides a Retry-After response header, the next retry will be delayed by the duration specified by the header value."
},
"MaxDelay": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "The maximum permissible delay between retry attempts when the service does not provide a Retry-After response header.\nIf the service provides a Retry-After response header, the next retry will be delayed by the duration specified by the header value."
},
"MaxRetries": {
@@ -94,7 +94,7 @@
},
"NetworkTimeout": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "The timeout applied to an individual network operations."
}
},
diff --git a/src/Components/Aspire.Azure.Messaging.EventHubs/ConfigurationSchema.json b/src/Components/Aspire.Azure.Messaging.EventHubs/ConfigurationSchema.json
index 8a7aa73e659..8a2fb3ccfe1 100644
--- a/src/Components/Aspire.Azure.Messaging.EventHubs/ConfigurationSchema.json
+++ b/src/Components/Aspire.Azure.Messaging.EventHubs/ConfigurationSchema.json
@@ -41,7 +41,7 @@
"properties": {
"ConnectionIdleTimeout": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "The amount of time to allow a connection to have no observed traffic before considering\nit idle and eligible to close."
},
"CustomEndpointAddress": {
@@ -76,12 +76,12 @@
"properties": {
"Delay": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "The delay between retry attempts for a fixed approach or the delay\non which to base calculations for a backoff-based approach."
},
"MaximumDelay": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "The maximum permissible delay between retry attempts."
},
"MaximumRetries": {
@@ -97,7 +97,7 @@
},
"TryTimeout": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "The maximum duration to wait for completion of a single attempt, whether the initial\nattempt or a retry."
}
},
@@ -140,7 +140,7 @@
"properties": {
"ConnectionIdleTimeout": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "The amount of time to allow a connection to have no observed traffic before considering\nit idle and eligible to close."
},
"CustomEndpointAddress": {
@@ -175,12 +175,12 @@
"properties": {
"Delay": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "The delay between retry attempts for a fixed approach or the delay\non which to base calculations for a backoff-based approach."
},
"MaximumDelay": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "The maximum permissible delay between retry attempts."
},
"MaximumRetries": {
@@ -196,7 +196,7 @@
},
"TryTimeout": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "The maximum duration to wait for completion of a single attempt, whether the initial\nattempt or a retry."
}
},
@@ -247,7 +247,7 @@
"properties": {
"ConnectionIdleTimeout": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "The amount of time to allow a connection to have no observed traffic before considering\nit idle and eligible to close."
},
"CustomEndpointAddress": {
@@ -286,17 +286,17 @@
},
"LoadBalancingUpdateInterval": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "The desired amount of time to allow between load balancing verification attempts."
},
"MaximumWaitTime": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "The maximum amount of time to wait for an event to become available for a given partition before emitting\nan empty event."
},
"PartitionOwnershipExpirationInterval": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "The desired amount of time to consider a partition owned by a specific event processor\ninstance before the ownership is considered stale and the partition becomes eligible to be\nrequested by another event processor that wishes to assume responsibility for processing it."
},
"PrefetchCount": {
@@ -312,12 +312,12 @@
"properties": {
"Delay": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "The delay between retry attempts for a fixed approach or the delay\non which to base calculations for a backoff-based approach."
},
"MaximumDelay": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "The maximum permissible delay between retry attempts."
},
"MaximumRetries": {
@@ -333,7 +333,7 @@
},
"TryTimeout": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "The maximum duration to wait for completion of a single attempt, whether the initial\nattempt or a retry."
}
},
@@ -380,7 +380,7 @@
"properties": {
"ConnectionIdleTimeout": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "The amount of time to allow a connection to have no observed traffic before considering\nit idle and eligible to close."
},
"CustomEndpointAddress": {
@@ -408,7 +408,7 @@
},
"DefaultMaximumReceiveWaitTime": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "The default amount of time to wait for the requested amount of messages when reading; if this\nperiod elapses before the requested amount of messages were available or read, then the set of\nmessages that were read will be returned."
},
"Identifier": {
@@ -432,12 +432,12 @@
"properties": {
"Delay": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "The delay between retry attempts for a fixed approach or the delay\non which to base calculations for a backoff-based approach."
},
"MaximumDelay": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "The maximum permissible delay between retry attempts."
},
"MaximumRetries": {
@@ -453,7 +453,7 @@
},
"TryTimeout": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "The maximum duration to wait for completion of a single attempt, whether the initial\nattempt or a retry."
}
},
diff --git a/src/Components/Aspire.Azure.Messaging.ServiceBus/AspireServiceBusExtensions.cs b/src/Components/Aspire.Azure.Messaging.ServiceBus/AspireServiceBusExtensions.cs
index 9f846d99e89..e1860e5bee7 100644
--- a/src/Components/Aspire.Azure.Messaging.ServiceBus/AspireServiceBusExtensions.cs
+++ b/src/Components/Aspire.Azure.Messaging.ServiceBus/AspireServiceBusExtensions.cs
@@ -22,23 +22,6 @@ public static class AspireServiceBusExtensions
{
private const string DefaultConfigSectionName = "Aspire:Azure:Messaging:ServiceBus";
- ///
- /// Registers as a singleton in the services provided by the .
- ///
- /// The to read config from and add services to.
- /// A name used to retrieve the connection string from the ConnectionStrings configuration section.
- /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration.
- /// An optional method that can be used for customizing the .
- /// Reads the configuration from "Aspire:Azure:Messaging:ServiceBus" section.
- /// Thrown when neither nor is provided.
- [Obsolete($"This method is obsolete and will be removed in a future version. Use {nameof(AddAzureServiceBusClient)} instead.")]
- public static void AddAzureServiceBus(
- this IHostApplicationBuilder builder,
- string connectionName,
- Action? configureSettings = null,
- Action>? configureClientBuilder = null)
- => AddAzureServiceBusClient(builder, connectionName, configureSettings, configureClientBuilder);
-
///
/// Registers as a singleton in the services provided by the .
///
@@ -57,23 +40,6 @@ public static void AddAzureServiceBusClient(
new MessageBusComponent().AddClient(builder, DefaultConfigSectionName, configureSettings, configureClientBuilder, connectionName, serviceKey: null);
}
- ///
- /// Registers as a singleton for given in the services provided by the .
- ///
- /// The to read config from and add services to.
- /// The name of the component, which is used as the of the service and also to retrieve the connection string from the ConnectionStrings configuration section.
- /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration.
- /// An optional method that can be used for customizing the .
- /// Reads the configuration from "Aspire:Azure:Messaging:ServiceBus:{name}" section.
- /// Thrown when neither nor is provided.
- [Obsolete($"This method is obsolete and will be removed in a future version. Use {nameof(AddKeyedAzureServiceBusClient)} instead.")]
- public static void AddKeyedAzureServiceBus(
- this IHostApplicationBuilder builder,
- string name,
- Action? configureSettings = null,
- Action>? configureClientBuilder = null)
- => AddKeyedAzureServiceBusClient(builder, name, configureSettings, configureClientBuilder);
-
///
/// Registers as a singleton for given in the services provided by the .
///
diff --git a/src/Components/Aspire.Azure.Messaging.ServiceBus/ConfigurationSchema.json b/src/Components/Aspire.Azure.Messaging.ServiceBus/ConfigurationSchema.json
index c2928f31ee9..da4f6f298d2 100644
--- a/src/Components/Aspire.Azure.Messaging.ServiceBus/ConfigurationSchema.json
+++ b/src/Components/Aspire.Azure.Messaging.ServiceBus/ConfigurationSchema.json
@@ -35,7 +35,7 @@
"properties": {
"ConnectionIdleTimeout": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "The amount of time to allow a connection to have no observed traffic before considering\nit idle and eligible to close."
},
"CustomEndpointAddress": {
@@ -56,12 +56,12 @@
"properties": {
"Delay": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "The delay between retry attempts for a fixed approach or the delay\non which to base calculations for a backoff-based approach."
},
"MaxDelay": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "The maximum permissible delay between retry attempts."
},
"MaxRetries": {
@@ -77,7 +77,7 @@
},
"TryTimeout": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "The maximum duration to wait for completion of a single attempt, whether the initial\nattempt or a retry."
}
},
diff --git a/src/Components/Aspire.Azure.Search.Documents/AspireAzureSearchExtensions.cs b/src/Components/Aspire.Azure.Search.Documents/AspireAzureSearchExtensions.cs
index 35c305c0158..94cad12f11b 100644
--- a/src/Components/Aspire.Azure.Search.Documents/AspireAzureSearchExtensions.cs
+++ b/src/Components/Aspire.Azure.Search.Documents/AspireAzureSearchExtensions.cs
@@ -23,22 +23,6 @@ public static class AspireAzureSearchExtensions
{
private const string DefaultConfigSectionName = "Aspire:Azure:Search:Documents";
- ///
- /// Registers as a singleton in the services provided by the .
- ///
- /// The to read config from and add services to.
- /// A name used to retrieve the connection string from the ConnectionStrings configuration section.
- /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration.
- /// An optional method that can be used for customizing the .
- /// Reads the configuration from "Aspire:Azure:Search:Documents" section.
- [Obsolete($"This method is obsolete and will be removed in a future version. Use {nameof(AddAzureSearchClient)} instead.")]
- public static void AddAzureSearch(
- this IHostApplicationBuilder builder,
- string connectionName,
- Action? configureSettings = null,
- Action>? configureClientBuilder = null)
- => AddAzureSearchClient(builder, connectionName, configureSettings, configureClientBuilder);
-
///
/// Registers as a singleton in the services provided by the .
///
@@ -56,22 +40,6 @@ public static void AddAzureSearchClient(
new AzureSearchComponent().AddClient(builder, DefaultConfigSectionName, configureSettings, configureClientBuilder, connectionName, serviceKey: null);
}
- ///
- /// Registers as a singleton for given in the services provided by the .
- ///
- /// The to read config from and add services to.
- /// The name of the component, which is used as the of the service and also to retrieve the connection string from the ConnectionStrings configuration section.
- /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration.
- /// An optional method that can be used for customizing the .
- /// Reads the configuration from "Aspire:Azure:Search:Documents:{name}" section.
- [Obsolete($"This method is obsolete and will be removed in a future version. Use {nameof(AddKeyedAzureSearchClient)} instead.")]
- public static void AddKeyedAzureSearch(
- this IHostApplicationBuilder builder,
- string name,
- Action? configureSettings = null,
- Action>? configureClientBuilder = null)
- => AddKeyedAzureSearchClient(builder, name, configureSettings, configureClientBuilder);
-
///
/// Registers as a singleton for given in the services provided by the .
///
diff --git a/src/Components/Aspire.Azure.Search.Documents/ConfigurationSchema.json b/src/Components/Aspire.Azure.Search.Documents/ConfigurationSchema.json
index a8af51424cb..02844aec42f 100644
--- a/src/Components/Aspire.Azure.Search.Documents/ConfigurationSchema.json
+++ b/src/Components/Aspire.Azure.Search.Documents/ConfigurationSchema.json
@@ -72,12 +72,12 @@
"properties": {
"Delay": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "The delay between retry attempts for a fixed approach or the delay\non which to base calculations for a backoff-based approach.\nIf the service provides a Retry-After response header, the next retry will be delayed by the duration specified by the header value."
},
"MaxDelay": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "The maximum permissible delay between retry attempts when the service does not provide a Retry-After response header.\nIf the service provides a Retry-After response header, the next retry will be delayed by the duration specified by the header value."
},
"MaxRetries": {
@@ -93,7 +93,7 @@
},
"NetworkTimeout": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "The timeout applied to an individual network operations."
}
},
diff --git a/src/Components/Aspire.Azure.Security.KeyVault/AspireKeyVaultExtensions.cs b/src/Components/Aspire.Azure.Security.KeyVault/AspireKeyVaultExtensions.cs
index 826b6d86edf..912cfa3615e 100644
--- a/src/Components/Aspire.Azure.Security.KeyVault/AspireKeyVaultExtensions.cs
+++ b/src/Components/Aspire.Azure.Security.KeyVault/AspireKeyVaultExtensions.cs
@@ -23,24 +23,6 @@ public static class AspireKeyVaultExtensions
{
internal const string DefaultConfigSectionName = "Aspire:Azure:Security:KeyVault";
- ///
- /// Registers as a singleton in the services provided by the .
- /// Enables retries, corresponding health check, logging and telemetry.
- ///
- /// The to read config from and add services to.
- /// A name used to retrieve the connection string from the ConnectionStrings configuration section.
- /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration.
- /// An optional method that can be used for customizing the .
- /// Reads the configuration from "Aspire:Azure:Security:KeyVault" section.
- /// Thrown when mandatory is not provided.
- [Obsolete($"This method is obsolete and will be removed in a future version. Use {nameof(AddAzureKeyVaultClient)} instead.")]
- public static void AddAzureKeyVaultSecrets(
- this IHostApplicationBuilder builder,
- string connectionName,
- Action? configureSettings = null,
- Action>? configureClientBuilder = null)
- => AddAzureKeyVaultClient(builder, connectionName, configureSettings, configureClientBuilder);
-
///
/// Registers as a singleton in the services provided by the .
/// Enables retries, corresponding health check, logging and telemetry.
@@ -60,24 +42,6 @@ public static void AddAzureKeyVaultClient(
new KeyVaultComponent().AddClient(builder, DefaultConfigSectionName, configureSettings, configureClientBuilder, connectionName, serviceKey: null);
}
- ///
- /// Registers as a singleton for given in the services provided by the .
- /// Enables retries, corresponding health check, logging and telemetry.
- ///
- /// The to read config from and add services to.
- /// The name of the component, which is used as the of the service and also to retrieve the connection information from the ConnectionStrings configuration section.
- /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration.
- /// An optional method that can be used for customizing the .
- /// Reads the configuration from "Aspire:Azure:Security:KeyVault:{name}" section.
- /// Thrown when mandatory is not provided.
- [Obsolete($"This method is obsolete and will be removed in a future version. Use {nameof(AddKeyedAzureKeyVaultClient)} instead.")]
- public static void AddKeyedAzureKeyVaultSecrets(
- this IHostApplicationBuilder builder,
- string name,
- Action? configureSettings = null,
- Action>? configureClientBuilder = null)
- => AddKeyedAzureKeyVaultClient(builder, name, configureSettings, configureClientBuilder);
-
///
/// Registers as a singleton for given in the services provided by the .
/// Enables retries, corresponding health check, logging and telemetry.
@@ -101,23 +65,6 @@ public static void AddKeyedAzureKeyVaultClient(
new KeyVaultComponent().AddClient(builder, configurationSectionName, configureSettings, configureClientBuilder, connectionName: name, serviceKey: name);
}
- ///
- /// Adds the Azure KeyVault secrets to be configuration values in the .
- ///
- /// The to add the secrets to.
- /// A name used to retrieve the connection string from the ConnectionStrings configuration section.
- /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration.
- /// An optional method that can be used for customizing the .
- /// An optional instance to configure the behavior of the configuration provider.
- [Obsolete($"This method is obsolete and will be removed in a future version. Use {nameof(AddAzureKeyVaultSecrets)} instead.")]
- public static void AddKeyVaultSecrets(
- this IConfigurationManager configurationManager,
- string connectionName,
- Action? configureSettings = null,
- Action? configureClientOptions = null,
- AzureKeyVaultConfigurationOptions? options = null)
- => AddAzureKeyVaultSecrets(configurationManager, connectionName, configureSettings, configureClientOptions, options);
-
///
/// Adds the Azure KeyVault secrets to be configuration values in the .
///
diff --git a/src/Components/Aspire.Azure.Security.KeyVault/ConfigurationSchema.json b/src/Components/Aspire.Azure.Security.KeyVault/ConfigurationSchema.json
index dc868a6c6f8..4d1c4a0045c 100644
--- a/src/Components/Aspire.Azure.Security.KeyVault/ConfigurationSchema.json
+++ b/src/Components/Aspire.Azure.Security.KeyVault/ConfigurationSchema.json
@@ -73,12 +73,12 @@
"properties": {
"Delay": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "The delay between retry attempts for a fixed approach or the delay\non which to base calculations for a backoff-based approach.\nIf the service provides a Retry-After response header, the next retry will be delayed by the duration specified by the header value."
},
"MaxDelay": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "The maximum permissible delay between retry attempts when the service does not provide a Retry-After response header.\nIf the service provides a Retry-After response header, the next retry will be delayed by the duration specified by the header value."
},
"MaxRetries": {
@@ -94,7 +94,7 @@
},
"NetworkTimeout": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "The timeout applied to an individual network operations."
}
},
diff --git a/src/Components/Aspire.Azure.Storage.Blobs/AspireBlobStorageExtensions.cs b/src/Components/Aspire.Azure.Storage.Blobs/AspireBlobStorageExtensions.cs
index 7fd3120e6d0..ce840e79224 100644
--- a/src/Components/Aspire.Azure.Storage.Blobs/AspireBlobStorageExtensions.cs
+++ b/src/Components/Aspire.Azure.Storage.Blobs/AspireBlobStorageExtensions.cs
@@ -21,24 +21,6 @@ public static class AspireBlobStorageExtensions
{
private const string DefaultConfigSectionName = "Aspire:Azure:Storage:Blobs";
- ///
- /// Registers as a singleton in the services provided by the .
- /// Enables retries, corresponding health check, logging and telemetry.
- ///
- /// The to read config from and add services to.
- /// A name used to retrieve the connection string from the ConnectionStrings configuration section.
- /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration.
- /// An optional method that can be used for customizing the .
- /// Reads the configuration from "Aspire:Azure:Storage:Blobs" section.
- /// Thrown when neither nor is provided.
- [Obsolete($"This method is obsolete and will be removed in a future version. Use {nameof(AddAzureBlobClient)} instead.")]
- public static void AddAzureBlobService(
- this IHostApplicationBuilder builder,
- string connectionName,
- Action? configureSettings = null,
- Action>? configureClientBuilder = null)
- => AddAzureBlobClient(builder, connectionName, configureSettings, configureClientBuilder);
-
///
/// Registers as a singleton in the services provided by the .
/// Enables retries, corresponding health check, logging and telemetry.
@@ -58,24 +40,6 @@ public static void AddAzureBlobClient(
new BlobStorageComponent().AddClient(builder, DefaultConfigSectionName, configureSettings, configureClientBuilder, connectionName, serviceKey: null);
}
- ///
- /// Registers as a singleton for given in the services provided by the .
- /// Enables retries, corresponding health check, logging and telemetry.
- ///
- /// The to read config from and add services to.
- /// The name of the component, which is used as the of the service and also to retrieve the connection string from the ConnectionStrings configuration section.
- /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration.
- /// An optional method that can be used for customizing the .
- /// Reads the configuration from "Aspire:Azure:Storage:Blobs:{name}" section.
- /// Thrown when neither nor is provided.
- [Obsolete($"This method is obsolete and will be removed in a future version. Use {nameof(AddKeyedAzureBlobClient)} instead.")]
- public static void AddKeyedAzureBlobService(
- this IHostApplicationBuilder builder,
- string name,
- Action? configureSettings = null,
- Action>? configureClientBuilder = null)
- => AddKeyedAzureBlobClient(builder, name, configureSettings, configureClientBuilder);
-
///
/// Registers as a singleton for given in the services provided by the .
/// Enables retries, corresponding health check, logging and telemetry.
diff --git a/src/Components/Aspire.Azure.Storage.Blobs/ConfigurationSchema.json b/src/Components/Aspire.Azure.Storage.Blobs/ConfigurationSchema.json
index ab8f64a328f..464434c69e2 100644
--- a/src/Components/Aspire.Azure.Storage.Blobs/ConfigurationSchema.json
+++ b/src/Components/Aspire.Azure.Storage.Blobs/ConfigurationSchema.json
@@ -82,12 +82,12 @@
"properties": {
"Delay": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "The delay between retry attempts for a fixed approach or the delay\non which to base calculations for a backoff-based approach.\nIf the service provides a Retry-After response header, the next retry will be delayed by the duration specified by the header value."
},
"MaxDelay": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "The maximum permissible delay between retry attempts when the service does not provide a Retry-After response header.\nIf the service provides a Retry-After response header, the next retry will be delayed by the duration specified by the header value."
},
"MaxRetries": {
@@ -103,7 +103,7 @@
},
"NetworkTimeout": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "The timeout applied to an individual network operations."
}
},
diff --git a/src/Components/Aspire.Azure.Storage.Queues/AspireQueueStorageExtensions.cs b/src/Components/Aspire.Azure.Storage.Queues/AspireQueueStorageExtensions.cs
index 869d71c65bd..05b7415d5ef 100644
--- a/src/Components/Aspire.Azure.Storage.Queues/AspireQueueStorageExtensions.cs
+++ b/src/Components/Aspire.Azure.Storage.Queues/AspireQueueStorageExtensions.cs
@@ -22,24 +22,6 @@ public static class AspireQueueStorageExtensions
{
private const string DefaultConfigSectionName = "Aspire:Azure:Storage:Queues";
- ///
- /// Registers as a singleton in the services provided by the .
- /// Enables retries, corresponding health check, logging and telemetry.
- ///
- /// The to read config from and add services to.
- /// A name used to retrieve the connection string from the ConnectionStrings configuration section.
- /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration.
- /// An optional method that can be used for customizing the .
- /// Reads the configuration from "Aspire:Azure:Storage:Queues" section.
- /// Thrown when neither nor is provided.
- [Obsolete($"This method is obsolete and will be removed in a future version. Use {nameof(AddAzureQueueClient)} instead.")]
- public static void AddAzureQueueService(
- this IHostApplicationBuilder builder,
- string connectionName,
- Action? configureSettings = null,
- Action>? configureClientBuilder = null)
- => AddAzureQueueClient(builder, connectionName, configureSettings, configureClientBuilder);
-
///
/// Registers as a singleton in the services provided by the .
/// Enables retries, corresponding health check, logging and telemetry.
@@ -59,24 +41,6 @@ public static void AddAzureQueueClient(
new StorageQueueComponent().AddClient(builder, DefaultConfigSectionName, configureSettings, configureClientBuilder, connectionName, serviceKey: null);
}
- ///
- /// Registers as a singleton for given in the services provided by the .
- /// Enables retries, corresponding health check, logging and telemetry.
- ///
- /// The to read config from and add services to.
- /// The name of the component, which is used as the of the service and also to retrieve the connection string from the ConnectionStrings configuration section.
- /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration.
- /// An optional method that can be used for customizing the .
- /// Reads the configuration from "Aspire:Azure:Storage:Queues:{name}" section.
- /// Thrown when neither nor is provided.
- [Obsolete($"This method is obsolete and will be removed in a future version. Use {nameof(AddKeyedAzureQueueClient)} instead.")]
- public static void AddKeyedAzureQueueService(
- this IHostApplicationBuilder builder,
- string name,
- Action? configureSettings = null,
- Action>? configureClientBuilder = null)
- => AddKeyedAzureQueueClient(builder, name, configureSettings, configureClientBuilder);
-
///
/// Registers as a singleton for given in the services provided by the .
/// Enables retries, corresponding health check, logging and telemetry.
diff --git a/src/Components/Aspire.Azure.Storage.Queues/ConfigurationSchema.json b/src/Components/Aspire.Azure.Storage.Queues/ConfigurationSchema.json
index 88f50fb474d..41dd2137b2a 100644
--- a/src/Components/Aspire.Azure.Storage.Queues/ConfigurationSchema.json
+++ b/src/Components/Aspire.Azure.Storage.Queues/ConfigurationSchema.json
@@ -85,12 +85,12 @@
"properties": {
"Delay": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "The delay between retry attempts for a fixed approach or the delay\non which to base calculations for a backoff-based approach.\nIf the service provides a Retry-After response header, the next retry will be delayed by the duration specified by the header value."
},
"MaxDelay": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "The maximum permissible delay between retry attempts when the service does not provide a Retry-After response header.\nIf the service provides a Retry-After response header, the next retry will be delayed by the duration specified by the header value."
},
"MaxRetries": {
@@ -106,7 +106,7 @@
},
"NetworkTimeout": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "The timeout applied to an individual network operations."
}
},
diff --git a/src/Components/Aspire.Microsoft.Azure.Cosmos/AspireAzureCosmosDBExtensions.cs b/src/Components/Aspire.Microsoft.Azure.Cosmos/AspireAzureCosmosDBExtensions.cs
index 4f6faf3894a..6c52f21e321 100644
--- a/src/Components/Aspire.Microsoft.Azure.Cosmos/AspireAzureCosmosDBExtensions.cs
+++ b/src/Components/Aspire.Microsoft.Azure.Cosmos/AspireAzureCosmosDBExtensions.cs
@@ -17,24 +17,6 @@ public static class AspireAzureCosmosDBExtensions
{
private const string DefaultConfigSectionName = "Aspire:Microsoft:Azure:Cosmos";
- ///
- /// Registers as a singleton in the services provided by the .
- /// Configures logging and telemetry for the .
- ///
- /// The to read config from and add services to.
- /// The connection name to use to find a connection string.
- /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration.
- /// An optional method that can be used for customizing the .
- /// Reads the configuration from "Aspire:Microsoft:Azure:Cosmos" section.
- /// If required ConnectionString is not provided in configuration section
- [Obsolete($"This method is obsolete and will be removed in a future version. Use {nameof(AddAzureCosmosDBClient)} instead.")]
- public static void AddAzureCosmosDB(
- this IHostApplicationBuilder builder,
- string connectionName,
- Action? configureSettings = null,
- Action? configureClientOptions = null)
- => AddAzureCosmosDBClient(builder, connectionName, configureSettings, configureClientOptions);
-
///
/// Registers as a singleton in the services provided by the .
/// Configures logging and telemetry for the .
@@ -54,25 +36,6 @@ public static void AddAzureCosmosDBClient(
AddAzureCosmosDB(builder, DefaultConfigSectionName, configureSettings, configureClientOptions, connectionName, serviceKey: null);
}
- ///
- /// Registers as a singleton for given in the services provided by the .
- /// Configures logging and telemetry for the .
- ///
- /// The to read config from and add services to.
- /// The name of the component, which is used as the of the service and also to retrieve the connection string from the ConnectionStrings configuration section.
- /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration.
- /// An optional method that can be used for customizing the .
- /// Reads the configuration from "Aspire:Microsoft:Azure:Cosmos:{name}" section.
- /// If required ConnectionString is not provided in configuration section
- [Obsolete($"This method is obsolete and will be removed in a future version. Use {nameof(AddKeyedAzureCosmosDbClient)} instead.")]
- public static void AddKeyedAzureCosmosDB(
- this IHostApplicationBuilder builder,
- string name,
- Action? configureSettings = null,
- Action? configureClientOptions = null)
- => AddKeyedAzureCosmosDbClient(builder, name, configureSettings, configureClientOptions);
-
-
///
/// Registers as a singleton for given in the services provided by the .
/// Configures logging and telemetry for the .
diff --git a/src/Components/Aspire.Microsoft.EntityFrameworkCore.Cosmos/ConfigurationSchema.json b/src/Components/Aspire.Microsoft.EntityFrameworkCore.Cosmos/ConfigurationSchema.json
index 34530922816..a01987c6825 100644
--- a/src/Components/Aspire.Microsoft.EntityFrameworkCore.Cosmos/ConfigurationSchema.json
+++ b/src/Components/Aspire.Microsoft.EntityFrameworkCore.Cosmos/ConfigurationSchema.json
@@ -54,7 +54,7 @@
},
"RequestTimeout": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "Gets or sets the time to wait for the response to come back from the network peer."
},
"Tracing": {
diff --git a/src/Components/Aspire.RabbitMQ.Client/AspireRabbitMQExtensions.cs b/src/Components/Aspire.RabbitMQ.Client/AspireRabbitMQExtensions.cs
index 9190ddefdcd..e03eaab6681 100644
--- a/src/Components/Aspire.RabbitMQ.Client/AspireRabbitMQExtensions.cs
+++ b/src/Components/Aspire.RabbitMQ.Client/AspireRabbitMQExtensions.cs
@@ -26,23 +26,6 @@ public static class AspireRabbitMQExtensions
private static readonly ActivitySource s_activitySource = new ActivitySource(ActivitySourceName);
private const string DefaultConfigSectionName = "Aspire:RabbitMQ:Client";
- ///
- /// Registers as a singleton in the services provided by the .
- /// Enables retries, corresponding health check, logging, and telemetry.
- ///
- /// The to read config from and add services to.
- /// A name used to retrieve the connection string from the ConnectionStrings configuration section.
- /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration.
- /// An optional method that can be used for customizing the . It's invoked after the options are read from the configuration.
- /// Reads the configuration from "Aspire:RabbitMQ:Client" section.
- [Obsolete($"This method is obsolete and will be removed in a future version. Use {nameof(AddRabbitMQClient)} instead.")]
- public static void AddRabbitMQ(
- this IHostApplicationBuilder builder,
- string connectionName,
- Action? configureSettings = null,
- Action? configureConnectionFactory = null)
- => AddRabbitMQClient(builder, connectionName, configureSettings, configureConnectionFactory);
-
///
/// Registers as a singleton in the services provided by the .
/// Enables retries, corresponding health check, logging, and telemetry.
@@ -59,23 +42,6 @@ public static void AddRabbitMQClient(
Action? configureConnectionFactory = null)
=> AddRabbitMQClient(builder, DefaultConfigSectionName, configureSettings, configureConnectionFactory, connectionName, serviceKey: null);
- ///
- /// Registers as a keyed singleton for the given in the services provided by the .
- /// Enables retries, corresponding health check, logging, and telemetry.
- ///
- /// The to read config from and add services to.
- /// The name of the component, which is used as the of the service and also to retrieve the connection string from the ConnectionStrings configuration section.
- /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration.
- /// An optional method that can be used for customizing the . It's invoked after the options are read from the configuration.
- /// Reads the configuration from "Aspire:RabbitMQ:Client:{name}" section.
- [Obsolete($"This method is obsolete and will be removed in a future version. Use {nameof(AddKeyedRabbitMQClient)} instead.")]
- public static void AddKeyedRabbitMQ(
- this IHostApplicationBuilder builder,
- string name,
- Action? configureSettings = null,
- Action? configureConnectionFactory = null)
- => AddKeyedRabbitMQClient(builder, name, configureSettings, configureConnectionFactory);
-
///
/// Registers as a keyed singleton for the given in the services provided by the .
/// Enables retries, corresponding health check, logging, and telemetry.
diff --git a/src/Components/Aspire.RabbitMQ.Client/ConfigurationSchema.json b/src/Components/Aspire.RabbitMQ.Client/ConfigurationSchema.json
index ea61b51bd1e..1f011967e03 100644
--- a/src/Components/Aspire.RabbitMQ.Client/ConfigurationSchema.json
+++ b/src/Components/Aspire.RabbitMQ.Client/ConfigurationSchema.json
@@ -48,7 +48,7 @@
},
"ContinuationTimeout": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "Amount of time protocol operations (e.g. queue.declare ) are allowed to take before\ntiming out."
},
"DefaultAddressFamily": {
@@ -208,7 +208,7 @@
},
"HandshakeContinuationTimeout": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "Amount of time protocol handshake operations are allowed to take before\ntiming out."
},
"HostName": {
@@ -221,7 +221,7 @@
},
"NetworkRecoveryInterval": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "Amount of time client will wait for before re-trying to recover connection."
},
"Password": {
@@ -238,7 +238,7 @@
},
"RequestedConnectionTimeout": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "Timeout setting for connection attempts."
},
"RequestedFrameMax": {
@@ -247,17 +247,17 @@
},
"RequestedHeartbeat": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "Heartbeat timeout to use when negotiating with the server."
},
"SocketReadTimeout": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "Timeout setting for socket read operations."
},
"SocketWriteTimeout": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "Timeout setting for socket write operations."
},
"Ssl": {
diff --git a/src/Components/Aspire.StackExchange.Redis/AspireRedisExtensions.cs b/src/Components/Aspire.StackExchange.Redis/AspireRedisExtensions.cs
index ed5a781df4f..d975658bef3 100644
--- a/src/Components/Aspire.StackExchange.Redis/AspireRedisExtensions.cs
+++ b/src/Components/Aspire.StackExchange.Redis/AspireRedisExtensions.cs
@@ -22,23 +22,6 @@ public static class AspireRedisExtensions
{
private const string DefaultConfigSectionName = "Aspire:StackExchange:Redis";
- ///
- /// Registers as a singleton in the services provided by the .
- /// Enables retries, corresponding health check, logging, and telemetry.
- ///
- /// The to read config from and add services to.
- /// A name used to retrieve the connection string from the ConnectionStrings configuration section.
- /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration.
- /// An optional method that can be used for customizing the . It's invoked after the options are read from the configuration.
- /// Reads the configuration from "Aspire:StackExchange:Redis" section.
- [Obsolete($"This method is obsolete and will be removed in a future version. Use {nameof(AddRedisClient)} instead.")]
- public static void AddRedis(
- this IHostApplicationBuilder builder,
- string connectionName,
- Action? configureSettings = null,
- Action? configureOptions = null)
- => AddRedisClient(builder, connectionName, configureSettings, configureOptions);
-
///
/// Registers as a singleton in the services provided by the .
/// Enables retries, corresponding health check, logging, and telemetry.
@@ -55,23 +38,6 @@ public static void AddRedisClient(
Action? configureOptions = null)
=> AddRedisClient(builder, DefaultConfigSectionName, configureSettings, configureOptions, connectionName, serviceKey: null);
- ///
- /// Registers as a keyed singleton for the given in the services provided by the .
- /// Enables retries, corresponding health check, logging, and telemetry.
- ///
- /// The to read config from and add services to.
- /// The name of the component, which is used as the of the service and also to retrieve the connection string from the ConnectionStrings configuration section.
- /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration.
- /// An optional method that can be used for customizing the . It's invoked after the options are read from the configuration.
- /// Reads the configuration from "Aspire:StackExchange:Redis:{name}" section.
- [Obsolete($"This method is obsolete and will be removed in a future version. Use {nameof(AddKeyedRedisClient)} instead.")]
- public static void AddKeyedRedis(
- this IHostApplicationBuilder builder,
- string name,
- Action? configureSettings = null,
- Action? configureOptions = null)
- => AddKeyedRedisClient(builder, name, configureSettings, configureOptions);
-
///
/// Registers as a keyed singleton for the given in the services provided by the .
/// Enables retries, corresponding health check, logging, and telemetry.
diff --git a/src/Components/Aspire.StackExchange.Redis/ConfigurationSchema.json b/src/Components/Aspire.StackExchange.Redis/ConfigurationSchema.json
index dd929753d46..3eff29f16bb 100644
--- a/src/Components/Aspire.StackExchange.Redis/ConfigurationSchema.json
+++ b/src/Components/Aspire.StackExchange.Redis/ConfigurationSchema.json
@@ -81,7 +81,7 @@
},
"HeartbeatInterval": {
"type": "string",
- "format": "duration",
+ "pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
"description": "Controls how often the connection heartbeats. A heartbeat includes:\n- Evaluating if any messages have timed out\n- Evaluating connection status (checking for failures)\n- Sending a server message to keep the connection alive if needed"
},
"IncludeDetailInExceptions": {
diff --git a/src/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs b/src/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs
index bcfa59056cc..b4c34ccb7c5 100644
--- a/src/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs
+++ b/src/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs
@@ -14,14 +14,6 @@ namespace Microsoft.Extensions.DependencyInjection;
///
public static class ServiceDiscoveryHttpClientBuilderExtensions
{
- ///
- /// Adds service discovery to the .
- ///
- /// The builder.
- /// The builder.
- [Obsolete(error: true, message: "This method is obsolete and will be removed in a future version. The recommended alternative is to use the 'AddServiceDiscovery' method instead.")]
- public static IHttpClientBuilder UseServiceDiscovery(this IHttpClientBuilder httpClientBuilder) => httpClientBuilder.AddServiceDiscovery();
-
///
/// Adds service discovery to the .
///
diff --git a/src/Tools/ConfigurationSchemaGenerator/ConfigSchemaEmitter.cs b/src/Tools/ConfigurationSchemaGenerator/ConfigSchemaEmitter.cs
index 71df90e55fd..8bac30cc785 100644
--- a/src/Tools/ConfigurationSchemaGenerator/ConfigSchemaEmitter.cs
+++ b/src/Tools/ConfigurationSchemaGenerator/ConfigSchemaEmitter.cs
@@ -401,12 +401,21 @@ private void AppendTypeNodes(JsonObject propertyNode, TypeSpec propertyTypeSpec)
}
}
+ const string NegativeOption = @"-?";
+ const string DaysAlone = @"\d{1,7}";
+ const string DaysPrefixOption = @$"({DaysAlone}[\.:])?";
+ const string MinutesOrSeconds = @"[0-5]?\d";
+ const string HourMinute = @$"([01]?\d|2[0-3]):{MinutesOrSeconds}";
+ const string HourMinuteSecond = HourMinute + $":{MinutesOrSeconds}";
+ const string SecondsFractionOption = @"(\.\d{1,7})?";
+ internal const string TimeSpanRegex = $"^{NegativeOption}({DaysAlone}|({DaysPrefixOption}({HourMinute}|{HourMinuteSecond}){SecondsFractionOption}))$";
+
private void AppendParsableFromString(JsonObject propertyNode, ParsableFromStringSpec parsable)
{
if (parsable.DisplayString == "TimeSpan")
{
propertyNode["type"] = "string";
- propertyNode["format"] = "duration";
+ propertyNode["pattern"] = TimeSpanRegex;
}
else if (parsable.StringParsableTypeKind == StringParsableTypeKind.Enum)
{
diff --git a/tests/Aspire.Azure.AI.OpenAI.Tests/ConformanceTests.cs b/tests/Aspire.Azure.AI.OpenAI.Tests/ConformanceTests.cs
index 61c3072b509..1437a26d551 100644
--- a/tests/Aspire.Azure.AI.OpenAI.Tests/ConformanceTests.cs
+++ b/tests/Aspire.Azure.AI.OpenAI.Tests/ConformanceTests.cs
@@ -33,11 +33,11 @@ public class ConformanceTests : ConformanceTests ne
("""{"Aspire": { "Azure": { "Data":{ "Tables": { "ServiceUri": "YOUR_URI"}}}}}""", "Value does not match format \"uri\""),
("""{"Aspire": { "Azure": { "Data":{ "Tables": { "ServiceUri": "http://YOUR_URI", "HealthChecks": "false"}}}}}""", "Value is \"string\" but should be \"boolean\""),
("""{"Aspire": { "Azure": { "Data":{ "Tables": { "ServiceUri": "http://YOUR_URI", "ClientOptions": {"Retry": {"Mode": "Fast"}}}}}}}""", "Value should match one of the values specified by the enum"),
- ("""{"Aspire": { "Azure": { "Data":{ "Tables": { "ServiceUri": "http://YOUR_URI", "ClientOptions": {"Retry": {"NetworkTimeout": "3S"}}}}}}}""", "Value does not match format \"duration\"")
+ ("""{"Aspire": { "Azure": { "Data":{ "Tables": { "ServiceUri": "http://YOUR_URI", "ClientOptions": {"Retry": {"NetworkTimeout": "3S"}}}}}}}""", "The string value is not a match for the indicated regular expression")
};
protected override string[] RequiredLogCategories => new string[]
diff --git a/tests/Aspire.Azure.Messaging.ServiceBus.Tests/ConformanceTests.cs b/tests/Aspire.Azure.Messaging.ServiceBus.Tests/ConformanceTests.cs
index 9ae3b4505e5..f47755aa6a4 100644
--- a/tests/Aspire.Azure.Messaging.ServiceBus.Tests/ConformanceTests.cs
+++ b/tests/Aspire.Azure.Messaging.ServiceBus.Tests/ConformanceTests.cs
@@ -34,11 +34,11 @@ public abstract class ConformanceTests : ConformanceTests ne
("""{"Aspire": { "Azure": { "Messaging":{ "ServiceBus": {"ClientOptions": {"CustomEndpointAddress": "EndPoint"}}}}}}""", "Value does not match format \"uri\""),
("""{"Aspire": { "Azure": { "Messaging":{ "ServiceBus": {"ClientOptions": {"EnableCrossEntityTransactions": "false"}}}}}}""", "Value is \"string\" but should be \"boolean\""),
("""{"Aspire": { "Azure": { "Messaging":{ "ServiceBus": {"ClientOptions": {"RetryOptions": {"Mode": "Fast"}}}}}}}""", "Value should match one of the values specified by the enum"),
- ("""{"Aspire": { "Azure": { "Messaging":{ "ServiceBus": {"ClientOptions": {"RetryOptions": {"TryTimeout": "3S"}}}}}}}""", "Value does not match format \"duration\""),
+ ("""{"Aspire": { "Azure": { "Messaging":{ "ServiceBus": {"ClientOptions": {"RetryOptions": {"TryTimeout": "3S"}}}}}}}""", "The string value is not a match for the indicated regular expression"),
("""{"Aspire": { "Azure": { "Messaging":{ "ServiceBus": {"ClientOptions": {"TransportType": "HTTP"}}}}}}""", "Value should match one of the values specified by the enum")
};
diff --git a/tests/Aspire.Azure.Search.Documents.Tests/ConformanceTests.cs b/tests/Aspire.Azure.Search.Documents.Tests/ConformanceTests.cs
index 34703d85822..ccfc0f39aa6 100644
--- a/tests/Aspire.Azure.Search.Documents.Tests/ConformanceTests.cs
+++ b/tests/Aspire.Azure.Search.Documents.Tests/ConformanceTests.cs
@@ -36,7 +36,7 @@ public class ConformanceTests : ConformanceTests ne
("""{"Aspire": { "Azure": { "Security":{ "KeyVault": { "VaultUri": "YOUR_URI"}}}}}""", "Value does not match format \"uri\""),
("""{"Aspire": { "Azure": { "Security":{ "KeyVault": { "VaultUri": "http://YOUR_URI", "HealthChecks": "false"}}}}}""", "Value is \"string\" but should be \"boolean\""),
("""{"Aspire": { "Azure": { "Security":{ "KeyVault": { "VaultUri": "http://YOUR_URI", "ClientOptions": {"Retry": {"Mode": "Fast"}}}}}}}""", "Value should match one of the values specified by the enum"),
- ("""{"Aspire": { "Azure": { "Security":{ "KeyVault": { "VaultUri": "http://YOUR_URI", "ClientOptions": {"Retry": {"NetworkTimeout": "3S"}}}}}}}""", "Value does not match format \"duration\"")
+ ("""{"Aspire": { "Azure": { "Security":{ "KeyVault": { "VaultUri": "http://YOUR_URI", "ClientOptions": {"Retry": {"NetworkTimeout": "3S"}}}}}}}""", "The string value is not a match for the indicated regular expression")
};
protected override void PopulateConfiguration(ConfigurationManager configuration, string? key = null)
diff --git a/tests/Aspire.Azure.Storage.Blobs.Tests/ConformanceTests.cs b/tests/Aspire.Azure.Storage.Blobs.Tests/ConformanceTests.cs
index 8223e4b499b..c0b8da41aac 100644
--- a/tests/Aspire.Azure.Storage.Blobs.Tests/ConformanceTests.cs
+++ b/tests/Aspire.Azure.Storage.Blobs.Tests/ConformanceTests.cs
@@ -47,7 +47,7 @@ public class ConformanceTests : ConformanceTests ne
("""{"Aspire": { "Azure": { "Storage":{ "Blobs": { "ServiceUri": "YOUR_URI"}}}}}""", "Value does not match format \"uri\""),
("""{"Aspire": { "Azure": { "Storage":{ "Blobs": { "ServiceUri": "http://YOUR_URI", "HealthChecks": "false"}}}}}""", "Value is \"string\" but should be \"boolean\""),
("""{"Aspire": { "Azure": { "Storage":{ "Blobs": { "ServiceUri": "http://YOUR_URI", "ClientOptions": {"Retry": {"Mode": "Fast"}}}}}}}""", "Value should match one of the values specified by the enum"),
- ("""{"Aspire": { "Azure": { "Storage":{ "Blobs": { "ServiceUri": "http://YOUR_URI", "ClientOptions": {"Retry": {"NetworkTimeout": "3S"}}}}}}}""", "Value does not match format \"duration\"")
+ ("""{"Aspire": { "Azure": { "Storage":{ "Blobs": { "ServiceUri": "http://YOUR_URI", "ClientOptions": {"Retry": {"NetworkTimeout": "3S"}}}}}}}""", "The string value is not a match for the indicated regular expression")
};
protected override void PopulateConfiguration(ConfigurationManager configuration, string? key = null)
diff --git a/tests/Aspire.Azure.Storage.Queues.Tests/ConformanceTests.cs b/tests/Aspire.Azure.Storage.Queues.Tests/ConformanceTests.cs
index c9e2b103662..31fd28e224d 100644
--- a/tests/Aspire.Azure.Storage.Queues.Tests/ConformanceTests.cs
+++ b/tests/Aspire.Azure.Storage.Queues.Tests/ConformanceTests.cs
@@ -48,7 +48,7 @@ public class ConformanceTests : ConformanceTests ne
("""{"Aspire": { "Azure": { "Storage":{ "Queues": { "ServiceUri": "http://YOUR_URI", "HealthChecks": "false"}}}}}""", "Value is \"string\" but should be \"boolean\""),
("""{"Aspire": { "Azure": { "Storage":{ "Queues": { "ClientOptions": {"MessageEncoding": "Fast"}}}}}}""", "Value should match one of the values specified by the enum"),
("""{"Aspire": { "Azure": { "Storage":{ "Queues": { "ClientOptions": {"Retry": {"Mode": "Fast"}}}}}}}""", "Value should match one of the values specified by the enum"),
- ("""{"Aspire": { "Azure": { "Storage":{ "Queues": { "ClientOptions": {"Retry": {"NetworkTimeout": "3S"}}}}}}}""", "Value does not match format \"duration\"")
+ ("""{"Aspire": { "Azure": { "Storage":{ "Queues": { "ClientOptions": {"Retry": {"NetworkTimeout": "PT3S"}}}}}}}""", "The string value is not a match for the indicated regular expression")
};
protected override void PopulateConfiguration(ConfigurationManager configuration, string? key = null)
diff --git a/tests/Aspire.Dashboard.Tests/Integration/StartupTests.cs b/tests/Aspire.Dashboard.Tests/Integration/StartupTests.cs
index b92a602a7a9..fe5cad06264 100644
--- a/tests/Aspire.Dashboard.Tests/Integration/StartupTests.cs
+++ b/tests/Aspire.Dashboard.Tests/Integration/StartupTests.cs
@@ -76,7 +76,7 @@ public async Task Configuration_OptionsMonitor_CanReadConfiguration()
additionalConfiguration: initialData =>
{
initialData["Dashboard:Otlp:AuthMode"] = nameof(OtlpAuthMode.ApiKey);
- initialData["Dashboard:Otlp:PrimaryApiKey"] = "abc123";
+ initialData["Dashboard:Otlp:PrimaryApiKey"] = "TestKey123!";
});
// Act
@@ -84,7 +84,7 @@ public async Task Configuration_OptionsMonitor_CanReadConfiguration()
// Assert
Assert.Equal(OtlpAuthMode.ApiKey, app.DashboardOptionsMonitor.CurrentValue.Otlp.AuthMode);
- Assert.Equal("abc123", app.DashboardOptionsMonitor.CurrentValue.Otlp.PrimaryApiKey);
+ Assert.Equal("TestKey123!", app.DashboardOptionsMonitor.CurrentValue.Otlp.PrimaryApiKey);
}
[Fact]
diff --git a/tests/Aspire.Hosting.Tests/Aspire.Hosting.Tests.csproj b/tests/Aspire.Hosting.Tests/Aspire.Hosting.Tests.csproj
index 2577ed2fa60..ef1d6964935 100644
--- a/tests/Aspire.Hosting.Tests/Aspire.Hosting.Tests.csproj
+++ b/tests/Aspire.Hosting.Tests/Aspire.Hosting.Tests.csproj
@@ -18,6 +18,7 @@
+
@@ -26,6 +27,7 @@
+
diff --git a/tests/Aspire.Hosting.Tests/Azure/AzureBicepResourceTests.cs b/tests/Aspire.Hosting.Tests/Azure/AzureBicepResourceTests.cs
index c3ff9bed959..5d0232be255 100644
--- a/tests/Aspire.Hosting.Tests/Azure/AzureBicepResourceTests.cs
+++ b/tests/Aspire.Hosting.Tests/Azure/AzureBicepResourceTests.cs
@@ -5,17 +5,20 @@
using System.Text.Json.Nodes;
using Aspire.Hosting.Azure;
+using Aspire.Hosting.Lifecycle;
using Aspire.Hosting.Tests.Utils;
using Aspire.Hosting.Utils;
using Azure.Provisioning.CognitiveServices;
using Azure.Provisioning.CosmosDB;
using Azure.Provisioning.Storage;
using Azure.ResourceManager.Storage.Models;
+using Microsoft.Extensions.DependencyInjection;
using Xunit;
+using Xunit.Abstractions;
namespace Aspire.Hosting.Tests.Azure;
-public class AzureBicepResourceTests
+public class AzureBicepResourceTests(ITestOutputHelper output)
{
[Fact]
public void AddBicepResource()
@@ -31,6 +34,42 @@ public void AddBicepResource()
Assert.Equal("value2", bicepResource.Resource.Parameters["param2"]);
}
+ [Fact]
+ public void AzureExtensionsAutomaticallyAddAzureProvisioning()
+ {
+ Action[] extensionCalls = [
+ (IDistributedApplicationBuilder builder) => builder.AddAzureAppConfiguration("x"),
+ (IDistributedApplicationBuilder builder) => builder.AddAzureApplicationInsights("x"),
+ (IDistributedApplicationBuilder builder) => builder.AddBicepTemplate("x", "template.bicep"),
+ (IDistributedApplicationBuilder builder) => builder.AddBicepTemplateString("x", "content"),
+ (IDistributedApplicationBuilder builder) => builder.AddAzureConstruct("x", _ => { }),
+ (IDistributedApplicationBuilder builder) => builder.AddAzureOpenAI("x"),
+ (IDistributedApplicationBuilder builder) => builder.AddAzureOpenAI("x"),
+ (IDistributedApplicationBuilder builder) => builder.AddAzureCosmosDB("x"),
+ (IDistributedApplicationBuilder builder) => builder.AddAzureEventHubs("x"),
+ (IDistributedApplicationBuilder builder) => builder.AddAzureKeyVault("x"),
+ (IDistributedApplicationBuilder builder) => builder.AddAzureLogAnalyticsWorkspace("x"),
+ (IDistributedApplicationBuilder builder) => builder.AddPostgres("x").AsAzurePostgresFlexibleServer(),
+ (IDistributedApplicationBuilder builder) => builder.AddRedis("x").AsAzureRedis(),
+ (IDistributedApplicationBuilder builder) => builder.AddAzureSearch("x"),
+ (IDistributedApplicationBuilder builder) => builder.AddAzureServiceBus("x"),
+ (IDistributedApplicationBuilder builder) => builder.AddAzureSignalR("x"),
+ (IDistributedApplicationBuilder builder) => builder.AddSqlServer("x").AsAzureSqlDatabase(),
+ (IDistributedApplicationBuilder builder) => builder.AddAzureStorage("x"),
+ ];
+
+ foreach (var extensionCall in extensionCalls)
+ {
+ using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
+ extensionCall(builder);
+
+ var app = builder.Build();
+ var hooks = app.Services.GetServices();
+ var provisionerHook = hooks.OfType();
+ Assert.Single(provisionerHook);
+ }
+ }
+
[Fact]
public void GetOutputReturnsOutputValue()
{
@@ -179,8 +218,8 @@ param keyVaultName string
name: keyVaultName
}
- resource cosmosDBAccount_5pKmb8KAZ 'Microsoft.DocumentDB/databaseAccounts@2023-04-15' = {
- name: toLower(take(concat('cosmos', uniqueString(resourceGroup().id)), 24))
+ resource cosmosDBAccount_MZyw35gqp 'Microsoft.DocumentDB/databaseAccounts@2023-04-15' = {
+ name: toLower(take('cosmos${uniqueString(resourceGroup().id)}', 24))
location: location
tags: {
'aspire-resource-name': 'cosmos'
@@ -200,8 +239,8 @@ param keyVaultName string
}
}
- resource cosmosDBSqlDatabase_TRuxXYh2M 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-04-15' = {
- parent: cosmosDBAccount_5pKmb8KAZ
+ resource cosmosDBSqlDatabase_2kiHyuwCU 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-04-15' = {
+ parent: cosmosDBAccount_MZyw35gqp
name: 'mydatabase'
location: location
properties: {
@@ -216,11 +255,12 @@ param keyVaultName string
name: 'connectionString'
location: location
properties: {
- value: 'AccountEndpoint=${cosmosDBAccount_5pKmb8KAZ.properties.documentEndpoint};AccountKey=${cosmosDBAccount_5pKmb8KAZ.listkeys(cosmosDBAccount_5pKmb8KAZ.apiVersion).primaryMasterKey}'
+ value: 'AccountEndpoint=${cosmosDBAccount_MZyw35gqp.properties.documentEndpoint};AccountKey=${cosmosDBAccount_MZyw35gqp.listkeys(cosmosDBAccount_MZyw35gqp.apiVersion).primaryMasterKey}'
}
}
""";
+ output.WriteLine(manifest.BicepText);
Assert.Equal(expectedBicep, manifest.BicepText);
Assert.NotNull(callbackDatabases);
@@ -276,8 +316,8 @@ param principalId string
param principalType string
- resource appConfigurationStore_j2IqAZkBh 'Microsoft.AppConfiguration/configurationStores@2023-03-01' = {
- name: toLower(take(concat('appConfig', uniqueString(resourceGroup().id)), 24))
+ resource appConfigurationStore_xM7mBhesj 'Microsoft.AppConfiguration/configurationStores@2023-03-01' = {
+ name: toLower(take('appConfig${uniqueString(resourceGroup().id)}', 24))
location: location
tags: {
'aspire-resource-name': 'appConfig'
@@ -289,9 +329,9 @@ param principalType string
}
}
- resource roleAssignment_umUNaNdeG 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
- scope: appConfigurationStore_j2IqAZkBh
- name: guid(appConfigurationStore_j2IqAZkBh.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '5ae67dd6-50cb-40e7-96ff-dc2bfa4b606b'))
+ resource roleAssignment_3uatMWw7h 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ scope: appConfigurationStore_xM7mBhesj
+ name: guid(appConfigurationStore_xM7mBhesj.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '5ae67dd6-50cb-40e7-96ff-dc2bfa4b606b'))
properties: {
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '5ae67dd6-50cb-40e7-96ff-dc2bfa4b606b')
principalId: principalId
@@ -299,14 +339,15 @@ param principalType string
}
}
- output appConfigEndpoint string = appConfigurationStore_j2IqAZkBh.properties.endpoint
+ output appConfigEndpoint string = appConfigurationStore_xM7mBhesj.properties.endpoint
""";
+ output.WriteLine(manifest.BicepText);
Assert.Equal(expectedBicep, manifest.BicepText);
}
[Fact]
- public async Task AddApplicationInsights()
+ public async Task AddApplicationInsightsWithoutExplicitLawGetsDefaultLawParameter()
{
using var builder = TestDistributedApplicationBuilder.Create();
@@ -325,7 +366,75 @@ public async Task AddApplicationInsights()
{
"type": "azure.bicep.v0",
"connectionString": "{appInsights.outputs.appInsightsConnectionString}",
- "path": "appInsights.module.bicep"
+ "path": "appInsights.module.bicep",
+ "params": {
+ "logAnalyticsWorkspaceId": ""
+ }
+ }
+ """;
+ Assert.Equal(expectedManifest, appInsightsManifest.ManifestNode.ToString());
+
+ var expectedBicep = """
+ targetScope = 'resourceGroup'
+
+ @description('')
+ param location string = resourceGroup().location
+
+ @description('')
+ param applicationType string = 'web'
+
+ @description('')
+ param kind string = 'web'
+
+ @description('')
+ param logAnalyticsWorkspaceId string
+
+
+ resource applicationInsightsComponent_eYAu4rv7j 'Microsoft.Insights/components@2020-02-02' = {
+ name: toLower(take('appInsights${uniqueString(resourceGroup().id)}', 24))
+ location: location
+ tags: {
+ 'aspire-resource-name': 'appInsights'
+ }
+ kind: kind
+ properties: {
+ Application_Type: applicationType
+ WorkspaceResourceId: logAnalyticsWorkspaceId
+ }
+ }
+
+ output appInsightsConnectionString string = applicationInsightsComponent_eYAu4rv7j.properties.ConnectionString
+
+ """;
+ output.WriteLine(appInsightsManifest.BicepText);
+ Assert.Equal(expectedBicep, appInsightsManifest.BicepText);
+ }
+
+ [Fact]
+ public async Task AddApplicationInsightsWithExplicitLawArgumentDoesntGetDefaultParameter()
+ {
+ using var builder = TestDistributedApplicationBuilder.Create();
+
+ var law = builder.AddAzureLogAnalyticsWorkspace("mylaw");
+ var appInsights = builder.AddAzureApplicationInsights("appInsights", law);
+
+ appInsights.Resource.Outputs["appInsightsConnectionString"] = "myinstrumentationkey";
+
+ var connectionStringResource = (IResourceWithConnectionString)appInsights.Resource;
+
+ Assert.Equal("appInsights", appInsights.Resource.Name);
+ Assert.Equal("myinstrumentationkey", await connectionStringResource.GetConnectionStringAsync());
+ Assert.Equal("{appInsights.outputs.appInsightsConnectionString}", appInsights.Resource.ConnectionStringExpression.ValueExpression);
+
+ var appInsightsManifest = await ManifestUtils.GetManifestWithBicep(appInsights.Resource);
+ var expectedManifest = """
+ {
+ "type": "azure.bicep.v0",
+ "connectionString": "{appInsights.outputs.appInsightsConnectionString}",
+ "path": "appInsights.module.bicep",
+ "params": {
+ "logAnalyticsWorkspaceId": "{mylaw.outputs.logAnalyticsWorkspaceId}"
+ }
}
""";
Assert.Equal(expectedManifest, appInsightsManifest.ManifestNode.ToString());
@@ -346,8 +455,8 @@ public async Task AddApplicationInsights()
param logAnalyticsWorkspaceId string
- resource applicationInsightsComponent_fo9MneV12 'Microsoft.Insights/components@2020-02-02' = {
- name: toLower(take(concat('appInsights', uniqueString(resourceGroup().id)), 24))
+ resource applicationInsightsComponent_eYAu4rv7j 'Microsoft.Insights/components@2020-02-02' = {
+ name: toLower(take('appInsights${uniqueString(resourceGroup().id)}', 24))
location: location
tags: {
'aspire-resource-name': 'appInsights'
@@ -359,9 +468,10 @@ param logAnalyticsWorkspaceId string
}
}
- output appInsightsConnectionString string = applicationInsightsComponent_fo9MneV12.properties.ConnectionString
+ output appInsightsConnectionString string = applicationInsightsComponent_eYAu4rv7j.properties.ConnectionString
""";
+ output.WriteLine(appInsightsManifest.BicepText);
Assert.Equal(expectedBicep, appInsightsManifest.BicepText);
}
@@ -391,8 +501,8 @@ public async Task AddLogAnalyticsWorkspace()
param location string = resourceGroup().location
- resource operationalInsightsWorkspace_uzGUFQdnZ 'Microsoft.OperationalInsights/workspaces@2022-10-01' = {
- name: toLower(take(concat('logAnalyticsWorkspace', uniqueString(resourceGroup().id)), 24))
+ resource operationalInsightsWorkspace_DuWNVIPPL 'Microsoft.OperationalInsights/workspaces@2022-10-01' = {
+ name: toLower(take('logAnalyticsWorkspace${uniqueString(resourceGroup().id)}', 24))
location: location
tags: {
'aspire-resource-name': 'logAnalyticsWorkspace'
@@ -404,9 +514,10 @@ public async Task AddLogAnalyticsWorkspace()
}
}
- output logAnalyticsWorkspaceId string = operationalInsightsWorkspace_uzGUFQdnZ.id
+ output logAnalyticsWorkspaceId string = operationalInsightsWorkspace_DuWNVIPPL.id
""";
+ output.WriteLine(appInsightsManifest.BicepText);
Assert.Equal(expectedBicep, appInsightsManifest.BicepText);
}
@@ -563,8 +674,8 @@ param keyVaultName string
name: keyVaultName
}
- resource redisCache_p9fE6TK3F 'Microsoft.Cache/Redis@2020-06-01' = {
- name: toLower(take(concat('cache', uniqueString(resourceGroup().id)), 24))
+ resource redisCache_enclX3umP 'Microsoft.Cache/Redis@2020-06-01' = {
+ name: toLower(take('cache${uniqueString(resourceGroup().id)}', 24))
location: location
tags: {
'aspire-resource-name': 'cache'
@@ -585,11 +696,12 @@ param keyVaultName string
name: 'connectionString'
location: location
properties: {
- value: '${redisCache_p9fE6TK3F.properties.hostName},ssl=true,password=${redisCache_p9fE6TK3F.listKeys(redisCache_p9fE6TK3F.apiVersion).primaryKey}'
+ value: '${redisCache_enclX3umP.properties.hostName},ssl=true,password=${redisCache_enclX3umP.listKeys(redisCache_enclX3umP.apiVersion).primaryKey}'
}
}
""";
+ output.WriteLine(manifest.BicepText);
Assert.Equal(expectedBicep, manifest.BicepText);
}
@@ -628,8 +740,8 @@ param principalId string
param principalType string
- resource keyVault_IKWI2x0B5 'Microsoft.KeyVault/vaults@2022-07-01' = {
- name: toLower(take(concat('mykv', uniqueString(resourceGroup().id)), 24))
+ resource keyVault_aMZbuK3Sy 'Microsoft.KeyVault/vaults@2022-07-01' = {
+ name: toLower(take('mykv${uniqueString(resourceGroup().id)}', 24))
location: location
tags: {
'aspire-resource-name': 'mykv'
@@ -637,16 +749,16 @@ param principalType string
properties: {
tenantId: tenant().tenantId
sku: {
- name: 'standard'
family: 'A'
+ name: 'standard'
}
enableRbacAuthorization: true
}
}
- resource roleAssignment_Z4xb36awa 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
- scope: keyVault_IKWI2x0B5
- name: guid(keyVault_IKWI2x0B5.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '00482a5a-887f-4fb3-b363-3b7fe8e74483'))
+ resource roleAssignment_hVU9zjQV1 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ scope: keyVault_aMZbuK3Sy
+ name: guid(keyVault_aMZbuK3Sy.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '00482a5a-887f-4fb3-b363-3b7fe8e74483'))
properties: {
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '00482a5a-887f-4fb3-b363-3b7fe8e74483')
principalId: principalId
@@ -654,9 +766,10 @@ param principalType string
}
}
- output vaultUri string = keyVault_IKWI2x0B5.properties.vaultUri
+ output vaultUri string = keyVault_aMZbuK3Sy.properties.vaultUri
""";
+ output.WriteLine(manifest.BicepText);
Assert.Equal(expectedBicep, manifest.BicepText);
}
@@ -695,8 +808,8 @@ param principalId string
param principalType string
- resource signalRService_hoCuRhvyj 'Microsoft.SignalRService/signalR@2022-02-01' = {
- name: toLower(take(concat('signalr', uniqueString(resourceGroup().id)), 24))
+ resource signalRService_iD3Yrl49T 'Microsoft.SignalRService/signalR@2022-02-01' = {
+ name: toLower(take('signalr${uniqueString(resourceGroup().id)}', 24))
location: location
tags: {
'aspire-resource-name': 'signalr'
@@ -721,9 +834,9 @@ param principalType string
}
}
- resource roleAssignment_O1jxNBUgA 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
- scope: signalRService_hoCuRhvyj
- name: guid(signalRService_hoCuRhvyj.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '420fcaa2-552c-430f-98ca-3264be4806c7'))
+ resource roleAssignment_35voRFfVj 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ scope: signalRService_iD3Yrl49T
+ name: guid(signalRService_iD3Yrl49T.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '420fcaa2-552c-430f-98ca-3264be4806c7'))
properties: {
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '420fcaa2-552c-430f-98ca-3264be4806c7')
principalId: principalId
@@ -731,9 +844,10 @@ param principalType string
}
}
- output hostName string = signalRService_hoCuRhvyj.properties.hostName
+ output hostName string = signalRService_iD3Yrl49T.properties.hostName
""";
+ output.WriteLine(manifest.BicepText);
Assert.Equal(expectedBicep, manifest.BicepText);
}
@@ -783,15 +897,14 @@ param principalName string
param principalType string
- resource sqlServer_l5O9GRsSn 'Microsoft.Sql/servers@2020-11-01-preview' = {
- name: toLower(take(concat('sql', uniqueString(resourceGroup().id)), 24))
+ resource sqlServer_lF9QWGqAt 'Microsoft.Sql/servers@2020-11-01-preview' = {
+ name: toLower(take('sql${uniqueString(resourceGroup().id)}', 24))
location: location
tags: {
'aspire-resource-name': 'sql'
}
properties: {
version: '12.0'
- minimalTlsVersion: '1.2'
publicNetworkAccess: 'Enabled'
administrators: {
administratorType: 'ActiveDirectory'
@@ -804,8 +917,8 @@ param principalType string
}
}
- resource sqlFirewallRule_Kr30BcxQt 'Microsoft.Sql/servers/firewallRules@2020-11-01-preview' = {
- parent: sqlServer_l5O9GRsSn
+ resource sqlFirewallRule_vcw7qNn72 'Microsoft.Sql/servers/firewallRules@2020-11-01-preview' = {
+ parent: sqlServer_lF9QWGqAt
name: 'AllowAllAzureIps'
properties: {
startIpAddress: '0.0.0.0'
@@ -813,8 +926,8 @@ param principalType string
}
}
- resource sqlFirewallRule_fA0ew2DcB 'Microsoft.Sql/servers/firewallRules@2020-11-01-preview' = {
- parent: sqlServer_l5O9GRsSn
+ resource sqlFirewallRule_IgqbBC6Hr 'Microsoft.Sql/servers/firewallRules@2020-11-01-preview' = {
+ parent: sqlServer_lF9QWGqAt
name: 'fw'
properties: {
startIpAddress: '0.0.0.0'
@@ -822,17 +935,18 @@ param principalType string
}
}
- resource sqlDatabase_F6FwuheAS 'Microsoft.Sql/servers/databases@2020-11-01-preview' = {
- parent: sqlServer_l5O9GRsSn
+ resource sqlDatabase_m3U42g9Y8 'Microsoft.Sql/servers/databases@2020-11-01-preview' = {
+ parent: sqlServer_lF9QWGqAt
name: 'dbName'
location: location
properties: {
}
}
- output sqlServerFqdn string = sqlServer_l5O9GRsSn.properties.fullyQualifiedDomainName
+ output sqlServerFqdn string = sqlServer_lF9QWGqAt.properties.fullyQualifiedDomainName
""";
+ output.WriteLine(manifest.BicepText);
Assert.Equal(expectedBicep, manifest.BicepText);
}
@@ -897,8 +1011,8 @@ param keyVaultName string
name: keyVaultName
}
- resource postgreSqlFlexibleServer_NYWb9Nbel 'Microsoft.DBforPostgreSQL/flexibleServers@2023-03-01-preview' = {
- name: toLower(take(concat('postgres', uniqueString(resourceGroup().id)), 24))
+ resource postgreSqlFlexibleServer_hFZg1J8nf 'Microsoft.DBforPostgreSQL/flexibleServers@2023-03-01-preview' = {
+ name: toLower(take('postgres${uniqueString(resourceGroup().id)}', 24))
location: location
tags: {
'aspire-resource-name': 'postgres'
@@ -925,8 +1039,8 @@ param keyVaultName string
}
}
- resource postgreSqlFirewallRule_2vbo6vMGo 'Microsoft.DBforPostgreSQL/flexibleServers/firewallRules@2023-03-01-preview' = {
- parent: postgreSqlFlexibleServer_NYWb9Nbel
+ resource postgreSqlFirewallRule_t5EgXW1q4 'Microsoft.DBforPostgreSQL/flexibleServers/firewallRules@2023-03-01-preview' = {
+ parent: postgreSqlFlexibleServer_hFZg1J8nf
name: 'AllowAllAzureIps'
properties: {
startIpAddress: '0.0.0.0'
@@ -934,8 +1048,8 @@ param keyVaultName string
}
}
- resource postgreSqlFirewallRule_oFtHmDYkz 'Microsoft.DBforPostgreSQL/flexibleServers/firewallRules@2023-03-01-preview' = {
- parent: postgreSqlFlexibleServer_NYWb9Nbel
+ resource postgreSqlFirewallRule_T9qS4dcOa 'Microsoft.DBforPostgreSQL/flexibleServers/firewallRules@2023-03-01-preview' = {
+ parent: postgreSqlFlexibleServer_hFZg1J8nf
name: 'AllowAllIps'
properties: {
startIpAddress: '0.0.0.0'
@@ -943,8 +1057,8 @@ param keyVaultName string
}
}
- resource postgreSqlFlexibleServerDatabase_TDYmKfyJc 'Microsoft.DBforPostgreSQL/flexibleServers/databases@2023-03-01-preview' = {
- parent: postgreSqlFlexibleServer_NYWb9Nbel
+ resource postgreSqlFlexibleServerDatabase_QJSbpnLQ9 'Microsoft.DBforPostgreSQL/flexibleServers/databases@2023-03-01-preview' = {
+ parent: postgreSqlFlexibleServer_hFZg1J8nf
name: 'dbName'
properties: {
}
@@ -955,11 +1069,12 @@ param keyVaultName string
name: 'connectionString'
location: location
properties: {
- value: 'Host=${postgreSqlFlexibleServer_NYWb9Nbel.properties.fullyQualifiedDomainName};Username=${administratorLogin};Password=${administratorLoginPassword}'
+ value: 'Host=${postgreSqlFlexibleServer_hFZg1J8nf.properties.fullyQualifiedDomainName};Username=${administratorLogin};Password=${administratorLoginPassword}'
}
}
""";
+ output.WriteLine(manifest.BicepText);
Assert.Equal(expectedBicep, manifest.BicepText);
}
@@ -1113,8 +1228,8 @@ param principalId string
param principalType string
- resource serviceBusNamespace_RuSlLOK64 'Microsoft.ServiceBus/namespaces@2022-10-01-preview' = {
- name: toLower(take(concat('sb', uniqueString(resourceGroup().id)), 24))
+ resource serviceBusNamespace_1RzZvI0LZ 'Microsoft.ServiceBus/namespaces@2021-11-01' = {
+ name: toLower(take('sb${uniqueString(resourceGroup().id)}', 24))
location: location
tags: {
'aspire-resource-name': 'sb'
@@ -1123,13 +1238,12 @@ param principalType string
name: sku
}
properties: {
- minimumTlsVersion: '1.2'
}
}
- resource roleAssignment_IS9HJzhT8 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
- scope: serviceBusNamespace_RuSlLOK64
- name: guid(serviceBusNamespace_RuSlLOK64.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '090c5cfd-751d-490a-894a-3ce6f1109419'))
+ resource roleAssignment_GAWCqJpjI 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ scope: serviceBusNamespace_1RzZvI0LZ
+ name: guid(serviceBusNamespace_1RzZvI0LZ.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '090c5cfd-751d-490a-894a-3ce6f1109419'))
properties: {
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '090c5cfd-751d-490a-894a-3ce6f1109419')
principalId: principalId
@@ -1137,49 +1251,50 @@ param principalType string
}
}
- resource serviceBusQueue_XlB4dhrJO 'Microsoft.ServiceBus/namespaces/queues@2022-10-01-preview' = {
- parent: serviceBusNamespace_RuSlLOK64
+ resource serviceBusQueue_kQwbucWhl 'Microsoft.ServiceBus/namespaces/queues@2021-11-01' = {
+ parent: serviceBusNamespace_1RzZvI0LZ
name: 'queue1'
location: location
properties: {
}
}
- resource serviceBusQueue_Q6ytJFbRX 'Microsoft.ServiceBus/namespaces/queues@2022-10-01-preview' = {
- parent: serviceBusNamespace_RuSlLOK64
+ resource serviceBusQueue_4iiWSBLWy 'Microsoft.ServiceBus/namespaces/queues@2021-11-01' = {
+ parent: serviceBusNamespace_1RzZvI0LZ
name: 'queue2'
location: location
properties: {
}
}
- resource serviceBusTopic_Ghv0Edotu 'Microsoft.ServiceBus/namespaces/topics@2022-10-01-preview' = {
- parent: serviceBusNamespace_RuSlLOK64
+ resource serviceBusTopic_6HCaIBS2e 'Microsoft.ServiceBus/namespaces/topics@2021-11-01' = {
+ parent: serviceBusNamespace_1RzZvI0LZ
name: 't1'
location: location
properties: {
}
}
- resource serviceBusSubscription_uPeK9Nyv8 'Microsoft.ServiceBus/namespaces/topics/subscriptions@2022-10-01-preview' = {
- parent: serviceBusTopic_Ghv0Edotu
+ resource serviceBusSubscription_ZeDpI38Lv 'Microsoft.ServiceBus/namespaces/topics/subscriptions@2021-11-01' = {
+ parent: serviceBusTopic_6HCaIBS2e
name: 's3'
location: location
properties: {
}
}
- resource serviceBusTopic_v5qGIuxZg 'Microsoft.ServiceBus/namespaces/topics@2022-10-01-preview' = {
- parent: serviceBusNamespace_RuSlLOK64
+ resource serviceBusTopic_VUuvZGvsD 'Microsoft.ServiceBus/namespaces/topics@2021-11-01' = {
+ parent: serviceBusNamespace_1RzZvI0LZ
name: 't2'
location: location
properties: {
}
}
- output serviceBusEndpoint string = serviceBusNamespace_RuSlLOK64.properties.serviceBusEndpoint
+ output serviceBusEndpoint string = serviceBusNamespace_1RzZvI0LZ.properties.serviceBusEndpoint
""";
+ output.WriteLine(manifest.BicepText);
Assert.Equal(expectedBicep, manifest.BicepText);
}
@@ -1263,8 +1378,8 @@ param principalType string
param storagesku string
- resource storageAccount_65zdmu5tK 'Microsoft.Storage/storageAccounts@2022-09-01' = {
- name: toLower(take(concat('storage', uniqueString(resourceGroup().id)), 24))
+ resource storageAccount_1XR3Um8QY 'Microsoft.Storage/storageAccounts@2022-09-01' = {
+ name: toLower(take('storage${uniqueString(resourceGroup().id)}', 24))
location: location
tags: {
'aspire-resource-name': 'storage'
@@ -1278,16 +1393,16 @@ param storagesku string
}
}
- resource blobService_24WqMwYy8 'Microsoft.Storage/storageAccounts/blobServices@2022-09-01' = {
- parent: storageAccount_65zdmu5tK
+ resource blobService_vTLU20GRg 'Microsoft.Storage/storageAccounts/blobServices@2022-09-01' = {
+ parent: storageAccount_1XR3Um8QY
name: 'default'
properties: {
}
}
- resource roleAssignment_ryHNwVXTs 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
- scope: storageAccount_65zdmu5tK
- name: guid(storageAccount_65zdmu5tK.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe'))
+ resource roleAssignment_Gz09cEnxb 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ scope: storageAccount_1XR3Um8QY
+ name: guid(storageAccount_1XR3Um8QY.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe'))
properties: {
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')
principalId: principalId
@@ -1295,9 +1410,9 @@ param storagesku string
}
}
- resource roleAssignment_hqRD0luQx 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
- scope: storageAccount_65zdmu5tK
- name: guid(storageAccount_65zdmu5tK.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3'))
+ resource roleAssignment_HRj6MDafS 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ scope: storageAccount_1XR3Um8QY
+ name: guid(storageAccount_1XR3Um8QY.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3'))
properties: {
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3')
principalId: principalId
@@ -1305,9 +1420,9 @@ param storagesku string
}
}
- resource roleAssignment_5PGf5zmoW 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
- scope: storageAccount_65zdmu5tK
- name: guid(storageAccount_65zdmu5tK.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88'))
+ resource roleAssignment_r0wA6OpKE 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ scope: storageAccount_1XR3Um8QY
+ name: guid(storageAccount_1XR3Um8QY.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88'))
properties: {
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88')
principalId: principalId
@@ -1315,11 +1430,12 @@ param storagesku string
}
}
- output blobEndpoint string = storageAccount_65zdmu5tK.properties.primaryEndpoints.blob
- output queueEndpoint string = storageAccount_65zdmu5tK.properties.primaryEndpoints.queue
- output tableEndpoint string = storageAccount_65zdmu5tK.properties.primaryEndpoints.table
+ output blobEndpoint string = storageAccount_1XR3Um8QY.properties.primaryEndpoints.blob
+ output queueEndpoint string = storageAccount_1XR3Um8QY.properties.primaryEndpoints.queue
+ output tableEndpoint string = storageAccount_1XR3Um8QY.properties.primaryEndpoints.table
""";
+ output.WriteLine(storageManifest.BicepText);
Assert.Equal(expectedBicep, storageManifest.BicepText);
// Check blob resource.
@@ -1424,14 +1540,14 @@ param principalType string
param searchSku string
- resource searchService_7WkaGluF0 'Microsoft.Search/searchServices@2023-11-01' = {
- name: toLower(take(concat('search', uniqueString(resourceGroup().id)), 24))
+ resource searchService_j3umigYGT 'Microsoft.Search/searchServices@2023-11-01' = {
+ name: toLower(take('search${uniqueString(resourceGroup().id)}', 24))
location: location
tags: {
'aspire-resource-name': 'search'
}
sku: {
- name: 'basic'
+ name: searchSku
}
properties: {
replicaCount: 1
@@ -1441,9 +1557,9 @@ param searchSku string
}
}
- resource roleAssignment_7uytIREoa 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
- scope: searchService_7WkaGluF0
- name: guid(searchService_7WkaGluF0.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8ebe5a00-799e-43f5-93ac-243d3dce84a7'))
+ resource roleAssignment_f77ijNEYF 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ scope: searchService_j3umigYGT
+ name: guid(searchService_j3umigYGT.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8ebe5a00-799e-43f5-93ac-243d3dce84a7'))
properties: {
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8ebe5a00-799e-43f5-93ac-243d3dce84a7')
principalId: principalId
@@ -1451,9 +1567,9 @@ param searchSku string
}
}
- resource roleAssignment_QpFzCj55x 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
- scope: searchService_7WkaGluF0
- name: guid(searchService_7WkaGluF0.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7ca78c08-252a-4471-8644-bb5ff32d4ba0'))
+ resource roleAssignment_s0J7B4aGN 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ scope: searchService_j3umigYGT
+ name: guid(searchService_j3umigYGT.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7ca78c08-252a-4471-8644-bb5ff32d4ba0'))
properties: {
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7ca78c08-252a-4471-8644-bb5ff32d4ba0')
principalId: principalId
@@ -1461,9 +1577,10 @@ param searchSku string
}
}
- output connectionString string = 'Endpoint=https://${searchService_7WkaGluF0.name}.search.windows.net'
+ output connectionString string = 'Endpoint=https://${searchService_j3umigYGT.name}.search.windows.net'
""";
+ output.WriteLine(manifest.BicepText);
Assert.Equal(expectedBicep, manifest.BicepText);
}
@@ -1536,8 +1653,8 @@ param principalId string
param principalType string
- resource cognitiveServicesAccount_6g8jyEjX5 'Microsoft.CognitiveServices/accounts@2023-05-01' = {
- name: toLower(take(concat('openai', uniqueString(resourceGroup().id)), 24))
+ resource cognitiveServicesAccount_wXAGTFUId 'Microsoft.CognitiveServices/accounts@2023-05-01' = {
+ name: toLower(take('openai${uniqueString(resourceGroup().id)}', 24))
location: location
kind: 'OpenAI'
sku: {
@@ -1549,9 +1666,9 @@ param principalType string
}
}
- resource roleAssignment_X7ie0XqR2 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
- scope: cognitiveServicesAccount_6g8jyEjX5
- name: guid(cognitiveServicesAccount_6g8jyEjX5.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a001fd3d-188f-4b5d-821b-7da978bf7442'))
+ resource roleAssignment_Hsk8rxWY8 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
+ scope: cognitiveServicesAccount_wXAGTFUId
+ name: guid(cognitiveServicesAccount_wXAGTFUId.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a001fd3d-188f-4b5d-821b-7da978bf7442'))
properties: {
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a001fd3d-188f-4b5d-821b-7da978bf7442')
principalId: principalId
@@ -1559,8 +1676,8 @@ param principalType string
}
}
- resource cognitiveServicesAccountDeployment_paT2Ndfh7 'Microsoft.CognitiveServices/accounts/deployments@2023-05-01' = {
- parent: cognitiveServicesAccount_6g8jyEjX5
+ resource cognitiveServicesAccountDeployment_5K9aRgiZP 'Microsoft.CognitiveServices/accounts/deployments@2023-05-01' = {
+ parent: cognitiveServicesAccount_wXAGTFUId
name: 'mymodel'
sku: {
name: 'Basic'
@@ -1568,16 +1685,17 @@ param principalType string
}
properties: {
model: {
- name: 'gpt-35-turbo'
format: 'OpenAI'
+ name: 'gpt-35-turbo'
version: '0613'
}
}
}
- output connectionString string = 'Endpoint=${cognitiveServicesAccount_6g8jyEjX5.properties.endpoint}'
+ output connectionString string = 'Endpoint=${cognitiveServicesAccount_wXAGTFUId.properties.endpoint}'
""";
+ output.WriteLine(manifest.BicepText);
Assert.Equal(expectedBicep, manifest.BicepText);
}
}
diff --git a/tests/Aspire.Hosting.Tests/Dapr/DaprTests.cs b/tests/Aspire.Hosting.Tests/Dapr/DaprTests.cs
new file mode 100644
index 00000000000..aa8573a7947
--- /dev/null
+++ b/tests/Aspire.Hosting.Tests/Dapr/DaprTests.cs
@@ -0,0 +1,82 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Hosting.Dapr;
+using Aspire.Hosting.Tests.Utils;
+using Aspire.Hosting.Utils;
+using Microsoft.Extensions.DependencyInjection;
+using Xunit;
+
+namespace Aspire.Hosting.Tests.Dapr;
+
+public class DaprTests
+{
+ [Fact]
+ public async Task WithDaprSideCarAddsAnnotationAndSidecarResource()
+ {
+ using var builder = TestDistributedApplicationBuilder.Create();
+ builder.AddDapr(o =>
+ {
+ // Fake path to avoid throwing
+ o.DaprPath = "dapr";
+ });
+
+ builder.AddContainer("name", "image")
+ .WithEndpoint("http", e =>
+ {
+ e.Port = 8000;
+ e.AllocatedEndpoint = new(e, "localhost", 80);
+ })
+ .WithDaprSidecar();
+
+ using var app = builder.Build();
+ await app.ExecuteBeforeStartHooksAsync(default);
+
+ var model = app.Services.GetRequiredService();
+
+ Assert.Equal(3, model.Resources.Count);
+ var container = Assert.Single(model.Resources.OfType());
+ var sidecarResource = Assert.Single(model.Resources.OfType());
+ var sideCarCli = Assert.Single(model.Resources.OfType());
+
+ Assert.True(sideCarCli.TryGetEndpoints(out var endpoints));
+
+ var ports = new Dictionary
+ {
+ ["http"] = 3500,
+ ["grpc"] = 50001,
+ ["metrics"] = 9090
+ };
+
+ foreach (var e in endpoints)
+ {
+ e.AllocatedEndpoint = new(e, "localhost", ports[e.Name]);
+ }
+
+ var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(container);
+ var sidecarArgs = await ArgumentEvaluator.GetArgumentListAsync(sideCarCli);
+
+ Assert.Equal("http://localhost:3500", config["DAPR_HTTP_ENDPOINT"]);
+ Assert.Equal("http://localhost:50001", config["DAPR_GRPC_ENDPOINT"]);
+
+ var expectedArgs = new[]
+ {
+ "run",
+ "--app-id",
+ "name",
+ "--app-port",
+ "80",
+ "--dapr-grpc-port",
+ "{{- portForServing \"name-dapr-cli_grpc\" -}}",
+ "--dapr-http-port",
+ "{{- portForServing \"name-dapr-cli_http\" -}}",
+ "--metrics-port",
+ "{{- portForServing \"name-dapr-cli_metrics\" -}}",
+ "--app-channel-address",
+ "localhost"
+ };
+
+ Assert.Equal(expectedArgs, sidecarArgs);
+ Assert.NotNull(container.Annotations.OfType());
+ }
+}
diff --git a/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs b/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs
index d542ee0f217..13e59bea1c7 100644
--- a/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs
+++ b/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs
@@ -252,7 +252,7 @@ public async Task SpecifyingEnvPortInEndpointFlowsToEnv()
.WithHttpEndpoint(name: "http0", env: "PORT0");
testProgram.AppBuilder.AddContainer("redis0", "redis")
- .WithEndpoint(containerPort: 6379, name: "tcp", env: "REDIS_PORT");
+ .WithEndpoint(targetPort: 6379, name: "tcp", env: "REDIS_PORT");
await using var app = testProgram.Build();
diff --git a/tests/Aspire.Hosting.Tests/Kafka/AddKafkaTests.cs b/tests/Aspire.Hosting.Tests/Kafka/AddKafkaTests.cs
index 1d4cd26df19..e9b8e3c1aa6 100644
--- a/tests/Aspire.Hosting.Tests/Kafka/AddKafkaTests.cs
+++ b/tests/Aspire.Hosting.Tests/Kafka/AddKafkaTests.cs
@@ -24,7 +24,7 @@ public void AddKafkaContainerWithDefaultsAddsAnnotationMetadata()
Assert.Equal("kafka", containerResource.Name);
var endpoint = Assert.Single(containerResource.Annotations.OfType());
- Assert.Equal(9092, endpoint.ContainerPort);
+ Assert.Equal(9092, endpoint.TargetPort);
Assert.False(endpoint.IsExternal);
Assert.Equal("tcp", endpoint.Name);
Assert.Null(endpoint.Port);
@@ -79,7 +79,7 @@ public async Task VerifyManifest()
"scheme": "tcp",
"protocol": "tcp",
"transport": "tcp",
- "containerPort": 9092
+ "targetPort": 9092
}
}
}
diff --git a/tests/Aspire.Hosting.Tests/ManifestGenerationTests.cs b/tests/Aspire.Hosting.Tests/ManifestGenerationTests.cs
index 1cb7a21b019..154b6db07cc 100644
--- a/tests/Aspire.Hosting.Tests/ManifestGenerationTests.cs
+++ b/tests/Aspire.Hosting.Tests/ManifestGenerationTests.cs
@@ -85,7 +85,7 @@ public void EnsureWorkerProjectDoesNotGetBindingsGenerated()
public void EnsureExecutablesWithDockerfileProduceDockerfilev0Manifest()
{
using var program = CreateTestProgramJsonDocumentManifestPublisher(includeNodeApp: true);
- program.NodeAppBuilder!.WithHttpsEndpoint(containerPort: 3000, env: "HTTPS_PORT")
+ program.NodeAppBuilder!.WithHttpsEndpoint(targetPort: 3000, env: "HTTPS_PORT")
.PublishAsDockerFile();
// Build AppHost so that publisher can be resolved.
@@ -109,9 +109,9 @@ public void EnsureExecutablesWithDockerfileProduceDockerfilev0Manifest()
Assert.True(nodeapp.TryGetProperty("env", out var env));
Assert.True(nodeapp.TryGetProperty("bindings", out var bindings));
- Assert.Equal(3000, bindings.GetProperty("https").GetProperty("containerPort").GetInt32());
+ Assert.Equal(3000, bindings.GetProperty("https").GetProperty("targetPort").GetInt32());
Assert.Equal("https", bindings.GetProperty("https").GetProperty("scheme").GetString());
- Assert.Equal("{nodeapp.bindings.https.port}", env.GetProperty("HTTPS_PORT").GetString());
+ Assert.Equal("{nodeapp.bindings.https.targetPort}", env.GetProperty("HTTPS_PORT").GetString());
}
[Fact]
@@ -155,7 +155,7 @@ public void EnsureContainerWithEndpointsEmitsContainerPort()
var grafana = resources.GetProperty("grafana");
var bindings = grafana.GetProperty("bindings");
var httpBinding = bindings.GetProperty("http");
- Assert.Equal(3000, httpBinding.GetProperty("containerPort").GetInt32());
+ Assert.Equal(3000, httpBinding.GetProperty("targetPort").GetInt32());
}
[Fact]
@@ -456,9 +456,9 @@ public void NodeAppIsExecutableResource()
using var program = CreateTestProgramJsonDocumentManifestPublisher();
program.AppBuilder.AddNodeApp("nodeapp", "..\\foo\\app.js")
- .WithHttpEndpoint(hostPort: 5031, env: "PORT");
+ .WithHttpEndpoint(port: 5031, env: "PORT");
program.AppBuilder.AddNpmApp("npmapp", "..\\foo")
- .WithHttpEndpoint(hostPort: 5032, env: "PORT");
+ .WithHttpEndpoint(port: 5032, env: "PORT");
// Build AppHost so that publisher can be resolved.
program.Build();
@@ -482,7 +482,7 @@ static void AssertNodeResource(TestProgram program, string resourceName, JsonEle
Assert.Equal("http", httpBinding.GetProperty("scheme").GetString());
var env = jsonElement.GetProperty("env");
- Assert.Equal($$"""{{{resourceName}}.bindings.http.port}""", env.GetProperty("PORT").GetString());
+ Assert.Equal($$"""{{{resourceName}}.bindings.http.targetPort}""", env.GetProperty("PORT").GetString());
Assert.Equal(program.AppBuilder.Environment.EnvironmentName.ToLowerInvariant(), env.GetProperty("NODE_ENV").GetString());
var command = jsonElement.GetProperty("command");
@@ -648,7 +648,7 @@ public void VerifyTestProgramFullManifest()
"scheme": "tcp",
"protocol": "tcp",
"transport": "tcp",
- "containerPort": 1433
+ "targetPort": 1433
}
}
},
@@ -669,7 +669,7 @@ public void VerifyTestProgramFullManifest()
"scheme": "tcp",
"protocol": "tcp",
"transport": "tcp",
- "containerPort": 3306
+ "targetPort": 3306
}
}
},
@@ -686,7 +686,7 @@ public void VerifyTestProgramFullManifest()
"scheme": "tcp",
"protocol": "tcp",
"transport": "tcp",
- "containerPort": 6379
+ "targetPort": 6379
}
}
},
@@ -706,7 +706,7 @@ public void VerifyTestProgramFullManifest()
"scheme": "tcp",
"protocol": "tcp",
"transport": "tcp",
- "containerPort": 5432
+ "targetPort": 5432
}
}
},
@@ -727,7 +727,7 @@ public void VerifyTestProgramFullManifest()
"scheme": "tcp",
"protocol": "tcp",
"transport": "tcp",
- "containerPort": 5672
+ "targetPort": 5672
}
}
},
@@ -740,7 +740,7 @@ public void VerifyTestProgramFullManifest()
"scheme": "tcp",
"protocol": "tcp",
"transport": "tcp",
- "containerPort": 27017
+ "targetPort": 27017
}
}
},
@@ -760,7 +760,7 @@ public void VerifyTestProgramFullManifest()
"scheme": "tcp",
"protocol": "tcp",
"transport": "tcp",
- "containerPort": 1521
+ "targetPort": 1521
}
}
},
@@ -780,7 +780,7 @@ public void VerifyTestProgramFullManifest()
"scheme": "tcp",
"protocol": "tcp",
"transport": "tcp",
- "containerPort": 9092
+ "targetPort": 9092
}
}
},
diff --git a/tests/Aspire.Hosting.Tests/MongoDB/AddMongoDBTests.cs b/tests/Aspire.Hosting.Tests/MongoDB/AddMongoDBTests.cs
index 284b10b1991..83834ad7560 100644
--- a/tests/Aspire.Hosting.Tests/MongoDB/AddMongoDBTests.cs
+++ b/tests/Aspire.Hosting.Tests/MongoDB/AddMongoDBTests.cs
@@ -27,7 +27,7 @@ public void AddMongoDBContainerWithDefaultsAddsAnnotationMetadata()
Assert.Equal("mongodb", containerResource.Name);
var endpoint = Assert.Single(containerResource.Annotations.OfType());
- Assert.Equal(27017, endpoint.ContainerPort);
+ Assert.Equal(27017, endpoint.TargetPort);
Assert.False(endpoint.IsExternal);
Assert.Equal("tcp", endpoint.Name);
Assert.Null(endpoint.Port);
@@ -55,7 +55,7 @@ public void AddMongoDBContainerAddsAnnotationMetadata()
Assert.Equal("mongodb", containerResource.Name);
var endpoint = Assert.Single(containerResource.Annotations.OfType());
- Assert.Equal(27017, endpoint.ContainerPort);
+ Assert.Equal(27017, endpoint.TargetPort);
Assert.False(endpoint.IsExternal);
Assert.Equal("tcp", endpoint.Name);
Assert.Equal(9813, endpoint.Port);
@@ -161,7 +161,7 @@ public async Task VerifyManifest()
"scheme": "tcp",
"protocol": "tcp",
"transport": "tcp",
- "containerPort": 27017
+ "targetPort": 27017
}
}
}
diff --git a/tests/Aspire.Hosting.Tests/MySql/AddMySqlTests.cs b/tests/Aspire.Hosting.Tests/MySql/AddMySqlTests.cs
index cafc58398b7..2a7922fe4f8 100644
--- a/tests/Aspire.Hosting.Tests/MySql/AddMySqlTests.cs
+++ b/tests/Aspire.Hosting.Tests/MySql/AddMySqlTests.cs
@@ -32,7 +32,7 @@ public async Task AddMySqlContainerWithDefaultsAddsAnnotationMetadata()
Assert.Null(containerAnnotation.Registry);
var endpoint = Assert.Single(containerResource.Annotations.OfType());
- Assert.Equal(3306, endpoint.ContainerPort);
+ Assert.Equal(3306, endpoint.TargetPort);
Assert.False(endpoint.IsExternal);
Assert.Equal("tcp", endpoint.Name);
Assert.Null(endpoint.Port);
@@ -72,7 +72,7 @@ public async Task AddMySqlAddsAnnotationMetadata()
Assert.Null(containerAnnotation.Registry);
var endpoint = Assert.Single(containerResource.Annotations.OfType());
- Assert.Equal(3306, endpoint.ContainerPort);
+ Assert.Equal(3306, endpoint.TargetPort);
Assert.False(endpoint.IsExternal);
Assert.Equal("tcp", endpoint.Name);
Assert.Equal(1234, endpoint.Port);
@@ -154,7 +154,7 @@ public async Task VerifyManifest()
"scheme": "tcp",
"protocol": "tcp",
"transport": "tcp",
- "containerPort": 3306
+ "targetPort": 3306
}
}
}
@@ -192,7 +192,7 @@ public async Task VerifyManifestWithPasswordParameter()
"scheme": "tcp",
"protocol": "tcp",
"transport": "tcp",
- "containerPort": 3306
+ "targetPort": 3306
}
}
}
diff --git a/tests/Aspire.Hosting.Tests/Nats/AddNatsTests.cs b/tests/Aspire.Hosting.Tests/Nats/AddNatsTests.cs
index 83265d1a4d6..2912ad5214b 100644
--- a/tests/Aspire.Hosting.Tests/Nats/AddNatsTests.cs
+++ b/tests/Aspire.Hosting.Tests/Nats/AddNatsTests.cs
@@ -26,7 +26,7 @@ public void AddNatsContainerWithDefaultsAddsAnnotationMetadata()
Assert.Equal("nats", containerResource.Name);
var endpoint = Assert.Single(containerResource.Annotations.OfType());
- Assert.Equal(4222, endpoint.ContainerPort);
+ Assert.Equal(4222, endpoint.TargetPort);
Assert.False(endpoint.IsExternal);
Assert.Equal("tcp", endpoint.Name);
Assert.Null(endpoint.Port);
@@ -65,7 +65,7 @@ public void AddNatsContainerAddsAnnotationMetadata()
Assert.Equal("-js -sd /data".Split(' '), args);
var endpoint = Assert.Single(containerResource.Annotations.OfType());
- Assert.Equal(4222, endpoint.ContainerPort);
+ Assert.Equal(4222, endpoint.TargetPort);
Assert.False(endpoint.IsExternal);
Assert.Equal("tcp", endpoint.Name);
Assert.Equal(1234, endpoint.Port);
@@ -107,7 +107,7 @@ public async Task VerifyManifest()
"scheme": "tcp",
"protocol": "tcp",
"transport": "tcp",
- "containerPort": 4222
+ "targetPort": 4222
}
}
}
diff --git a/tests/Aspire.Hosting.Tests/Oracle/AddOracleDatabaseTests.cs b/tests/Aspire.Hosting.Tests/Oracle/AddOracleDatabaseTests.cs
index d9139b9c527..e7a14565d5a 100644
--- a/tests/Aspire.Hosting.Tests/Oracle/AddOracleDatabaseTests.cs
+++ b/tests/Aspire.Hosting.Tests/Oracle/AddOracleDatabaseTests.cs
@@ -30,7 +30,7 @@ public async Task AddOracleWithDefaultsAddsAnnotationMetadata()
Assert.Equal("container-registry.oracle.com", containerAnnotation.Registry);
var endpoint = Assert.Single(containerResource.Annotations.OfType());
- Assert.Equal(1521, endpoint.ContainerPort);
+ Assert.Equal(1521, endpoint.TargetPort);
Assert.False(endpoint.IsExternal);
Assert.Equal("tcp", endpoint.Name);
Assert.Null(endpoint.Port);
@@ -70,7 +70,7 @@ public async Task AddOracleAddsAnnotationMetadata()
Assert.Equal("container-registry.oracle.com", containerAnnotation.Registry);
var endpoint = Assert.Single(containerResource.Annotations.OfType());
- Assert.Equal(1521, endpoint.ContainerPort);
+ Assert.Equal(1521, endpoint.TargetPort);
Assert.False(endpoint.IsExternal);
Assert.Equal("tcp", endpoint.Name);
Assert.Equal(1234, endpoint.Port);
@@ -153,7 +153,7 @@ public async Task AddDatabaseToOracleDatabaseAddsAnnotationMetadata()
Assert.Equal("container-registry.oracle.com", containerAnnotation.Registry);
var endpoint = Assert.Single(containerResource.Annotations.OfType());
- Assert.Equal(1521, endpoint.ContainerPort);
+ Assert.Equal(1521, endpoint.TargetPort);
Assert.False(endpoint.IsExternal);
Assert.Equal("tcp", endpoint.Name);
Assert.Equal(1234, endpoint.Port);
@@ -194,7 +194,7 @@ public async Task VerifyManifest()
"scheme": "tcp",
"protocol": "tcp",
"transport": "tcp",
- "containerPort": 1521
+ "targetPort": 1521
}
}
}
@@ -232,7 +232,7 @@ public async Task VerifyManifestWithPasswordParameter()
"scheme": "tcp",
"protocol": "tcp",
"transport": "tcp",
- "containerPort": 1521
+ "targetPort": 1521
}
}
}
diff --git a/tests/Aspire.Hosting.Tests/PortAllocatorTest.cs b/tests/Aspire.Hosting.Tests/PortAllocatorTest.cs
new file mode 100644
index 00000000000..902736731ae
--- /dev/null
+++ b/tests/Aspire.Hosting.Tests/PortAllocatorTest.cs
@@ -0,0 +1,37 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Hosting.Publishing;
+using Xunit;
+
+namespace Aspire.Hosting.Tests;
+
+public class PortAllocatorTest
+{
+ [Fact]
+ public void CanAllocatePorts()
+ {
+ var allocator = new PortAllocator(1000);
+ var port1 = allocator.AllocatePort();
+ allocator.AddUsedPort(port1);
+ var port2 = allocator.AllocatePort();
+
+ Assert.Equal(1000, port1);
+ Assert.Equal(1001, port2);
+ }
+
+ [Fact]
+ public void SkipUsedPorts()
+ {
+ var allocator = new PortAllocator(1000);
+ allocator.AddUsedPort(1000);
+ allocator.AddUsedPort(1001);
+ allocator.AddUsedPort(1003);
+ var port1 = allocator.AllocatePort();
+ allocator.AddUsedPort(port1);
+ var port2 = allocator.AllocatePort();
+
+ Assert.Equal(1002, port1);
+ Assert.Equal(1004, port2);
+ }
+}
diff --git a/tests/Aspire.Hosting.Tests/Postgres/AddPostgresTests.cs b/tests/Aspire.Hosting.Tests/Postgres/AddPostgresTests.cs
index 8ab24752877..8445d0e248f 100644
--- a/tests/Aspire.Hosting.Tests/Postgres/AddPostgresTests.cs
+++ b/tests/Aspire.Hosting.Tests/Postgres/AddPostgresTests.cs
@@ -32,7 +32,7 @@ public async Task AddPostgresWithDefaultsAddsAnnotationMetadata()
Assert.Null(containerAnnotation.Registry);
var endpoint = Assert.Single(containerResource.Annotations.OfType());
- Assert.Equal(5432, endpoint.ContainerPort);
+ Assert.Equal(5432, endpoint.TargetPort);
Assert.False(endpoint.IsExternal);
Assert.Equal("tcp", endpoint.Name);
Assert.Null(endpoint.Port);
@@ -87,7 +87,7 @@ public async Task AddPostgresAddsAnnotationMetadata()
Assert.Null(containerAnnotation.Registry);
var endpoint = Assert.Single(containerResource.Annotations.OfType());
- Assert.Equal(5432, endpoint.ContainerPort);
+ Assert.Equal(5432, endpoint.TargetPort);
Assert.False(endpoint.IsExternal);
Assert.Equal("tcp", endpoint.Name);
Assert.Equal(1234, endpoint.Port);
@@ -179,7 +179,7 @@ public async Task AddDatabaseToPostgresAddsAnnotationMetadata()
Assert.Null(containerAnnotation.Registry);
var endpoint = Assert.Single(containerResource.Annotations.OfType());
- Assert.Equal(5432, endpoint.ContainerPort);
+ Assert.Equal(5432, endpoint.TargetPort);
Assert.False(endpoint.IsExternal);
Assert.Equal("tcp", endpoint.Name);
Assert.Equal(1234, endpoint.Port);
@@ -238,7 +238,7 @@ public async Task VerifyManifest()
"scheme": "tcp",
"protocol": "tcp",
"transport": "tcp",
- "containerPort": 5432
+ "targetPort": 5432
}
}
}
@@ -281,7 +281,7 @@ public async Task VerifyManifestWithParameters()
"scheme": "tcp",
"protocol": "tcp",
"transport": "tcp",
- "containerPort": 5432
+ "targetPort": 5432
}
}
}
@@ -307,7 +307,7 @@ public async Task VerifyManifestWithParameters()
"scheme": "tcp",
"protocol": "tcp",
"transport": "tcp",
- "containerPort": 5432
+ "targetPort": 5432
}
}
}
@@ -333,7 +333,7 @@ public async Task VerifyManifestWithParameters()
"scheme": "tcp",
"protocol": "tcp",
"transport": "tcp",
- "containerPort": 5432
+ "targetPort": 5432
}
}
}
diff --git a/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs b/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs
index 108a409ed98..661d97d0831 100644
--- a/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs
+++ b/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs
@@ -32,7 +32,7 @@ public void AddRabbitMQContainerWithDefaultsAddsAnnotationMetadata(bool withMana
Assert.Equal("rabbit", containerResource.Name);
var primaryEndpoint = Assert.Single(containerResource.Annotations.OfType