Building Custom Resources in .NET Aspire

Aspire ships with built-in support for Redis, PostgreSQL, RabbitMQ, and many other common infrastructure components. But what happens when you need something that does not have first-party support — say, MinIO for S3-compatible storage, or Seq for structured log aggregation? You build a custom resource.

The Simplest Custom Resource: A Container

The most common custom resource is a container. You can add any Docker container to your AppHost using AddContainer:

Example.cs
var builder = DistributedApplication.CreateBuilder(args);

var seq = builder.AddContainer("seq", "datalust/seq", "latest")
    .WithEnvironment("ACCEPT_EULA", "Y")
    .WithHttpEndpoint(port: 5341, targetPort: 80, name: "ui");

builder.Build().Run();

This works, but it is not reusable. If every project in your organisation uses Seq, you do not want everyone repeating this configuration. That is where custom resource types come in.

Creating a Custom Resource Type

A custom resource starts with a class that inherits from one of Aspire's base resource types. For a container-based resource, inherit from ContainerResource:

Example.cs
public class SeqResource : ContainerResource, IResourceWithConnectionString
{
    internal const string HttpEndpointName = "http";

    public SeqResource(string name) : base(name) { }

    public ReferenceExpression ConnectionStringExpression =>
        ReferenceExpression.Create(
            $"http://{new HostUrl(this.GetEndpoint(HttpEndpointName))}");
}

The IResourceWithConnectionString interface allows other resources to use WithReference to get the Seq endpoint injected as a connection string.

Adding Extension Methods

Wrap the resource creation in an extension method for a clean API:

Example.cs
public static class SeqResourceExtensions
{
    public static IResourceBuilder<SeqResource> AddSeq(
        this IDistributedApplicationBuilder builder,
        string name,
        int? port = null)
    {
        var resource = new SeqResource(name);

        return builder.AddResource(resource)
            .WithImage("datalust/seq")
            .WithImageTag("latest")
            .WithEnvironment("ACCEPT_EULA", "Y")
            .WithHttpEndpoint(
                port: port,
                targetPort: 80,
                name: SeqResource.HttpEndpointName);
    }
}

Now your AppHost reads cleanly:

Example.cs
var seq = builder.AddSeq("seq");

var api = builder.AddProject<Projects.MyApi>("api")
    .WithReference(seq);

A More Complete Example: MinIO

Let us build a custom resource for MinIO, an S3-compatible object store. MinIO needs two endpoints (API and console) and credentials:

Example.cs
public class MinioResource : ContainerResource, IResourceWithConnectionString
{
    internal const string ApiEndpointName = "api";
    internal const string ConsoleEndpointName = "console";

    public MinioResource(string name) : base(name) { }

    public ParameterResource AccessKey { get; set; } = default!;
    public ParameterResource SecretKey { get; set; } = default!;

    public ReferenceExpression ConnectionStringExpression =>
        ReferenceExpression.Create(
            $"endpoint=http://{new HostUrl(this.GetEndpoint(ApiEndpointName))};accessKey={AccessKey};secretKey={SecretKey}");
}
Example.cs
public static class MinioResourceExtensions
{
    public static IResourceBuilder<MinioResource> AddMinio(
        this IDistributedApplicationBuilder builder,
        string name,
        int? apiPort = null,
        int? consolePort = null)
    {
        var accessKey = builder.AddParameter("minio-access-key", secret: true);
        var secretKey = builder.AddParameter("minio-secret-key", secret: true);

        var resource = new MinioResource(name)
        {
            AccessKey = accessKey.Resource,
            SecretKey = secretKey.Resource
        };

        return builder.AddResource(resource)
            .WithImage("minio/minio")
            .WithImageTag("latest")
            .WithArgs("server", "/data", "--console-address", ":9001")
            .WithEnvironment("MINIO_ROOT_USER", accessKey)
            .WithEnvironment("MINIO_ROOT_PASSWORD", secretKey)
            .WithHttpEndpoint(port: apiPort, targetPort: 9000,
                name: MinioResource.ApiEndpointName)
            .WithHttpEndpoint(port: consolePort, targetPort: 9001,
                name: MinioResource.ConsoleEndpointName)
            .WithDataVolume("minio-data");
    }
}

Usage in the AppHost:

Example.cs
var minio = builder.AddMinio("storage");

var mediaService = builder.AddProject<Projects.MediaService>("media")
    .WithReference(minio);

Adding Health Checks

You can attach custom health checks to your resource so the dashboard accurately reports its state:

Example.cs
public static IResourceBuilder<MinioResource> AddMinio(
    this IDistributedApplicationBuilder builder, string name)
{
    // ... resource setup ...

    builder.Services.AddHealthChecks()
        .AddUrlGroup(new Uri("http://localhost:9000/minio/health/live"),
            name: $"{name}-health");

    return resourceBuilder;
}

Packaging for Reuse

Once your custom resource is stable, consider packaging it as a NuGet package. The convention is to create two packages:

  1. Hosting package (e.g., MyOrg.Aspire.Hosting.Minio) — contains the resource type and AddMinio extension method for the AppHost
  2. Client package (e.g., MyOrg.Aspire.Minio) — contains the AddMinioClient extension method for consuming services

This mirrors how the official Aspire components are structured.

Custom resources let you extend Aspire to match your exact infrastructure. Whether it is a niche database, a custom service mesh component, or an internal tool, the pattern is the same: define a resource, wrap it in an extension method, and let Aspire handle the rest.