diff --git a/Aspire.sln b/Aspire.sln
index bd9fcaa7abb..941f9fbf352 100644
--- a/Aspire.sln
+++ b/Aspire.sln
@@ -203,6 +203,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CosmosEndToEnd.ApiService",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Playground.ServiceDefaults", "playground\Playground.ServiceDefaults\Playground.ServiceDefaults.csproj", "{25208C6F-0A9D-4D60-9EDD-256C9891B1CD}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Minio", "src\Components\Aspire.Minio\Aspire.Minio.csproj", "{18508B84-93C1-4F56-8538-355DBD5248EA}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -541,6 +543,10 @@ Global
{25208C6F-0A9D-4D60-9EDD-256C9891B1CD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{25208C6F-0A9D-4D60-9EDD-256C9891B1CD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{25208C6F-0A9D-4D60-9EDD-256C9891B1CD}.Release|Any CPU.Build.0 = Release|Any CPU
+ {18508B84-93C1-4F56-8538-355DBD5248EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {18508B84-93C1-4F56-8538-355DBD5248EA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {18508B84-93C1-4F56-8538-355DBD5248EA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {18508B84-93C1-4F56-8538-355DBD5248EA}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -635,6 +641,7 @@ Global
{51DDD6BC-1D6C-466A-B509-FC49E3BD72E4} = {DBEDDF76-1C33-4943-8CCB-337A7D48AFF5}
{EABB20A8-CDA2-4AFE-A5B1-FB631200CD64} = {DBEDDF76-1C33-4943-8CCB-337A7D48AFF5}
{25208C6F-0A9D-4D60-9EDD-256C9891B1CD} = {D173887B-AF42-4576-B9C1-96B9E9B3D9C0}
+ {18508B84-93C1-4F56-8538-355DBD5248EA} = {27381127-6C45-4B4C-8F18-41FF48DFE4B2}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {6DCEDFEC-988E-4CB3-B45B-191EB5086E0C}
diff --git a/Directory.Packages.props b/Directory.Packages.props
index aa25f554f07..3a906136885 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -95,6 +95,7 @@
+
@@ -117,4 +118,4 @@
-
\ No newline at end of file
+
diff --git a/src/Aspire.Hosting/Minio/MinioBuilderExtensions.cs b/src/Aspire.Hosting/Minio/MinioBuilderExtensions.cs
new file mode 100644
index 00000000000..a94463d3311
--- /dev/null
+++ b/src/Aspire.Hosting/Minio/MinioBuilderExtensions.cs
@@ -0,0 +1,68 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Net.Sockets;
+using Aspire.Hosting.ApplicationModel;
+using Aspire.Hosting.Publishing;
+
+namespace Aspire.Hosting;
+
+///
+/// Provides extension methods for adding Minio resources to an .
+///
+public static class MinioBuilderExtensions
+{
+ private const string RootUserEnvVarName = "MINIO_ROOT_USER";
+ private const string RootPasswordEnvVarName = "MINIO_ROOT_PASSWORD";
+
+ ///
+ /// Adds a Minio container to the application model. The default image is "minio/minio" and the tag is "latest".
+ ///
+ /// 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 Minio Admin.
+ /// The host port for Minio.
+ /// The root user for the Minio server.
+ /// The password for the Minio root user.
+ /// A reference to the .
+ public static IResourceBuilder AddMinioContainer(
+ this IDistributedApplicationBuilder builder,
+ string name,
+ string rootUser,
+ string rootPassword,
+ int minioPort = 9000,
+ int minioAdminPort = 9001)
+ {
+ var minioContainer = new MinioContainerResource(name, rootUser, rootPassword);
+
+ return builder
+ .AddResource(minioContainer)
+ .WithManifestPublishingCallback(context => WriteMinioContainerToManifest(context, minioContainer))
+ .WithAnnotation(new EndpointAnnotation(ProtocolType.Tcp, port: minioPort, containerPort: 9000, name: "minio"))
+ .WithAnnotation(new EndpointAnnotation(ProtocolType.Tcp, port: minioAdminPort, containerPort: 9001, name: "minio", uriScheme: "http"))
+ .WithAnnotation(new ContainerImageAnnotation { Image = "minio/minio", Tag = "latest" })
+ .WithEnvironment("MINIO_ADDRESS", ":9000")
+ .WithEnvironment("MINIO_CONSOLE_ADDRESS", ":9001")
+ .WithEnvironment("MINIO_PROMETHEUS_AUTH_TYPE", "public")
+ .WithEnvironment(context =>
+ {
+ if (context.PublisherName == "manifest")
+ {
+ context.EnvironmentVariables.Add(RootUserEnvVarName, $"{{{minioContainer.Name}.inputs.rootUser}}");
+ context.EnvironmentVariables.Add(RootPasswordEnvVarName, $"{{{minioContainer.Name}.inputs.rootPassword}}");
+ }
+ else
+ {
+ context.EnvironmentVariables.Add(RootUserEnvVarName, minioContainer.RootUser);
+ context.EnvironmentVariables.Add(RootPasswordEnvVarName, minioContainer.RootPassword);
+ }
+ })
+ .WithArgs("server", "/data");
+ }
+
+ private static void WriteMinioContainerToManifest(ManifestPublishingContext context, MinioContainerResource resource)
+ {
+ // Want to see if there is interest
+ context.WriteContainer(resource);
+ }
+}
diff --git a/src/Aspire.Hosting/Minio/MinioContainerResource.cs b/src/Aspire.Hosting/Minio/MinioContainerResource.cs
new file mode 100644
index 00000000000..f047bb0adc9
--- /dev/null
+++ b/src/Aspire.Hosting/Minio/MinioContainerResource.cs
@@ -0,0 +1,18 @@
+// 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.ApplicationModel;
+
+public class MinioContainerResource(string name, string rootUser, string rootPassword) : ContainerResource(name)
+{
+ ///
+ /// The Minio root user.
+ ///
+ public string RootUser { get; } = rootUser;
+
+ ///
+ /// The Minio root password.
+ ///
+ public string RootPassword { get; } = rootPassword;
+
+}
diff --git a/src/Aspire.Hosting/Minio/MinioServerResource.cs b/src/Aspire.Hosting/Minio/MinioServerResource.cs
new file mode 100644
index 00000000000..9b9592e3534
--- /dev/null
+++ b/src/Aspire.Hosting/Minio/MinioServerResource.cs
@@ -0,0 +1,41 @@
+// 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;
+
+///
+/// A resource that represents a Minio server.
+///
+/// The name of the resource.
+public class MinioServerResource(string name) : Resource(name), IResourceWithConnectionString, IResourceWithEnvironment
+{
+ ///
+ /// Gets the connection string for the Minio server.
+ ///
+ /// A connection string for the Minio server in the form "http://host:port".
+ public string? GetConnectionString()
+ {
+ if (!this.TryGetAllocatedEndPoints(out var allocatedEndpoints))
+ {
+ throw new DistributedApplicationException($"Minio resource \"{Name}\" does not have endpoint annotation.");
+ }
+
+ // Assuming Minio runs on HTTP by default. Adjust if it uses HTTPS.
+ var endpoint = allocatedEndpoints.SingleOrDefault();
+ return endpoint != null ? $"http://{endpoint.EndPointString}" : null;
+ }
+
+ ///
+ /// Gets the service port for the Minio server.
+ ///
+ /// The service port used by the Minio server.
+ public int? GetServicePort()
+ {
+ if (!this.TryGetAllocatedEndPoints(out var allocatedEndpoints))
+ {
+ throw new DistributedApplicationException($"Minio resource \"{Name}\" does not have endpoint annotation.");
+ }
+
+ return allocatedEndpoints.SingleOrDefault()?.Port;
+ }
+}
diff --git a/src/Components/Aspire.Minio/Aspire.Minio.csproj b/src/Components/Aspire.Minio/Aspire.Minio.csproj
new file mode 100644
index 00000000000..ba691ab855f
--- /dev/null
+++ b/src/Components/Aspire.Minio/Aspire.Minio.csproj
@@ -0,0 +1,20 @@
+
+
+
+
+ $(NetCurrent)
+ true
+ $(ComponentCommonPackageTags) Minio
+ Minio based S3 client that integrates with Aspire, including healthchecks and metrics.
+ $(NoWarn);SYSLIB1100
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Components/Aspire.Minio/MinioClientBuilderExtensionMethods.cs b/src/Components/Aspire.Minio/MinioClientBuilderExtensionMethods.cs
new file mode 100644
index 00000000000..604f0b5c71c
--- /dev/null
+++ b/src/Components/Aspire.Minio/MinioClientBuilderExtensionMethods.cs
@@ -0,0 +1,41 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.Extensions.Hosting;
+using Aspire.Minio;
+using Minio;
+using Microsoft.Extensions.Configuration;
+
+namespace Aspire.Extensions.Hosting;
+
+public static class MinioClientBuilderExtensionMethods
+{
+ public static void AddMinio(this IHostApplicationBuilder builder, string configurationSectionName)
+ {
+
+ ArgumentNullException.ThrowIfNull(builder);
+
+ // Obtain the configuration settings for the Minio client.
+
+ MinioConfiguration minioSettings = new();
+
+ builder.Configuration.Bind(configurationSectionName, minioSettings);
+
+ var endpoint = minioSettings.Endpoint;
+ var accessKey = minioSettings.AccessKey;
+ var secretKey = minioSettings.SecretKey;
+
+ // Add the Minio client to the service collection.
+ builder.Services.AddMinio(configureClient => configureClient
+ .WithEndpoint(endpoint, 9000)
+ .WithSSL(false)
+ .WithCredentials(accessKey, secretKey));
+
+ // Add the Minio health check to the service collection.
+
+ // Add the Minio tracing to the service collection.
+
+ // Add the Minio metrics to the service collection.
+
+ }
+}
diff --git a/src/Components/Aspire.Minio/MinioConfiguration.cs b/src/Components/Aspire.Minio/MinioConfiguration.cs
new file mode 100644
index 00000000000..39de0dfc159
--- /dev/null
+++ b/src/Components/Aspire.Minio/MinioConfiguration.cs
@@ -0,0 +1,18 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Aspire.Minio;
+internal sealed class MinioConfiguration
+{
+ public string? Endpoint { get; set; }
+
+ public string? AccessKey { get; set; }
+
+ public string? SecretKey { get; set; }
+
+ public bool HealthChecks { get; set; } = true;
+
+ public bool Tracing { get; set; } = true;
+
+ public bool Metrics { get; set; } = true;
+}
diff --git a/src/Components/Aspire.Minio/MinioHealthCheck.cs b/src/Components/Aspire.Minio/MinioHealthCheck.cs
new file mode 100644
index 00000000000..8ef64e8ce47
--- /dev/null
+++ b/src/Components/Aspire.Minio/MinioHealthCheck.cs
@@ -0,0 +1,54 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+
+namespace Aspire.HealthChecks.Minio;
+public class MinioHealthCheck : IHealthCheck
+ {
+ private readonly HttpClient _httpClient;
+ private readonly Uri _minioHealthLiveUri;
+ private readonly Uri _minioHealthClusterUri;
+ private readonly Uri _minioHealthClusterReadUri;
+
+ public MinioHealthCheck(HttpClient httpClient, string minioBaseUrl)
+ {
+ _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
+ _minioHealthLiveUri = new Uri($"{minioBaseUrl}/minio/health/live");
+ _minioHealthClusterUri = new Uri($"{minioBaseUrl}/minio/health/cluster");
+ _minioHealthClusterReadUri = new Uri($"{minioBaseUrl}/minio/health/cluster/read");
+ }
+
+ public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ // Node Liveness Check
+ var livenessResponse = await _httpClient.GetAsync(_minioHealthLiveUri, cancellationToken).ConfigureAwait(true);
+ if (!livenessResponse.IsSuccessStatusCode)
+ {
+ return HealthCheckResult.Unhealthy("MinIO is not responding to liveness checks");
+ }
+
+ // Cluster Write Quorum Check
+ var clusterWriteResponse = await _httpClient.GetAsync(_minioHealthClusterUri, cancellationToken).ConfigureAwait(true);
+ if (!clusterWriteResponse.IsSuccessStatusCode)
+ {
+ return HealthCheckResult.Unhealthy("MinIO cluster does not have write quorum");
+ }
+
+ // Cluster Read Quorum Check
+ var clusterReadResponse = await _httpClient.GetAsync(_minioHealthClusterReadUri, cancellationToken).ConfigureAwait(true);
+ if (!clusterReadResponse.IsSuccessStatusCode)
+ {
+ return HealthCheckResult.Unhealthy("MinIO cluster does not have read quorum");
+ }
+
+ return HealthCheckResult.Healthy("MinIO is healthy");
+ }
+ catch (Exception ex)
+ {
+ return HealthCheckResult.Unhealthy("Error occurred while checking MinIO health", ex);
+ }
+ }
+ }
+