Aspire Port- part 2–implementation

While building distributed applications with .NET Aspire, I kept running into
the same annoyance: I wanted the same resource to come up on the same port every time I ran
my app locally — a SQLite web viewer, a Mongo Express UI, or some other sidecar tool that
I’d bookmarked or wired into a script. Aspire assigns ports dynamically by default, which is
great for avoiding conflicts, but it meant my bookmarks and scripts kept breaking between
runs.

So I built PortExtensionsAspire, a small NuGet package that solves this for me
by computing a deterministic port from a resource name: the same name always hashes
to the same port, on every run, on every machine. I also added a simple way to share those
computed ports with other resources as environment variables, since that’s the part I needed
most in my own AppHost.

Installation

I add the package to my Aspire AppHost project like this:

dotnet add package PortExtensionsAspire

Requirements

  • .NET 10.0 or later
  • Aspire 13.0 or later

How it works

I introduced a PortResource — a lightweight Aspire resource whose only job
is to remember a set of names and their computed ports. I register it with
AddPort(), reserve one or more deterministic ports with
WithDeterministicPortEnvironment, and finish the setup with
Construct(), which publishes the computed ports as environment variables on the
resource (so I can see them in the Aspire dashboard).

Internally, I hash each name with a stable (non-randomized) string hash algorithm and reduce
it into the 0–65535 UInt16 port range, so restarting my app, or running it on
a different machine, always gives me the same port for the same name.

Basic usage

Here’s a complete example, taken straight from my own AppHost, that reserves deterministic
ports for a SQLite web viewer and a Mongo UI, uses one of them directly as an endpoint port,
and forwards all of them as environment variables to another project:

var builder = DistributedApplication.CreateBuilder(args);

var ports = builder.AddPort()
    .WithDeterministicPortEnvironment("sqliteweb", "mongodb")
    .Construct();

builder.AddSqlite("sqlite")
    .WithSqliteWeb(c =>
    {
        c.WithHttpEndpoint(
            targetPort: 8080,
            name: "http",
            port: ports.Resource.GetDeterministicPort("sqliteweb"));
    });

builder.AddProject<Projects.ShowPort>("ShowPort")
    .WithPortReference(ports);

builder.Build().Run();

With this in place, ports.Resource.GetDeterministicPort("sqliteweb") returns the
same port number every time I start the app, so I can bookmark
http://localhost:<port> once and it keeps working across restarts.

Consuming the ports in another resource

Calling .WithPortReference(ports) on any resource injects every port I’ve
registered on the PortResource as an environment variable named
PORT_{name}. In my downstream project, I can then read them like any other
environment variable:

using System.Collections;

foreach (DictionaryEntry de in Environment.GetEnvironmentVariables())
{
    if (de.Key?.ToString()?.StartsWith("PORT") == true)
        Console.WriteLine(de.Key + " " + de.Value);
}

API summary

  • builder.AddPort() — registers the shared PortResource.
  • WithDeterministicPortEnvironment(params string[] names) — pre-computes and
    caches a deterministic port for each given name.
  • Construct() — publishes the currently registered ports as environment
    variables on the PortResource, so they show up in the Aspire dashboard.
  • PortResource.GetDeterministicPort(string name) — returns (and caches) the
    deterministic port for name, computing it on first use if it wasn’t already
    reserved.
  • WithPortReference(IResourceBuilder<PortResource> ports) — injects every
    registered port into the target resource as a PORT_{name} environment variable.

⚠ Important warning: ports are not guaranteed to be unique

I should be upfront about a limitation of my own design: the deterministic port for a name is
derived by hashing that name and reducing the result into the UInt16 range. This
guarantees the port is repeatable for a given name, but it does
not guarantee uniqueness across different names. Two
unrelated names can hash to the same port — a hash collision — which would cause
two different resources to end up with the same port number.

If I ever run into a collision myself, the workaround I use is simple: pick a different name
(or add a tag, using the two-argument overload GetDeterministicPort(name, tag))
for one of the colliding resources, or override the port manually for that resource.

// I combine a name with a tag to reduce the chance of collisions
// between similarly named resources across different environments.
var port = ports.Resource.GetDeterministicPort("sqliteweb");

Why I didn’t just hardcode ports

I could have hardcoded port numbers directly in my AppHost, but that would mean manually
tracking which ports are already used across every resource in my solution, and it doesn’t
scale well as I add more sample apps, integration tests, or side projects. Deriving the port
from the resource name removed that bookkeeping for me: as long as my names don’t collide,
ports stay stable and conflict-free without any manual coordination on my part.

Links

  • NuGet Package: https://www.nuget.org/packages/PortExtensionsAspire
  • GitHub Repository: https://github.com/ignatandrei/aspireExtensions

License

I released this project under the MIT License — feel free to use it in your own Aspire apps.


Posted

in

by

Tags: