.NET Aspire inner-loop networking overview

One of the advantages of developing with .NET Aspire is that it enables you to develop, test, and debug cloud-native apps locally. Inner-loop networking is a key aspect of .NET Aspire that allows your apps to communicate with each other in your development environment. In this article, you learn how .NET Aspire handles various networking scenarios with proxies, service bindings, endpoint configurations, and launch profiles.

Networking in the inner loop

The inner loop is the process of developing and testing your app locally before deploying it to a target environment. .NET Aspire provides several tools and features to simplify and enhance the networking experience in the inner loop, such as:

  • Launch profiles: Launch profiles are configuration files that specify how to run your app locally. You can use launch profiles (such as the launchSettings.json file) to define the service bindings, environment variables, and launch settings for your app.
  • Service bindings/Endpoint configurations: Service bindings are the connections between your app and the services it depends on, such as databases, message queues, or APIs. Service bindings provide information such as the service name, host port, scheme, and environment variable. You can add service bindings to your app either implicitly (via launch profiles) or explicitly by calling WithEndpoint.
  • Proxies: .NET Aspire automatically launches a proxy for each service binding you add to your app, and assigns a port for the proxy to listen on. The proxy then forwards the requests to the port that your app listens on, which might be different from the proxy port. This way, you can avoid port conflicts and access your app and services using consistent and predictable URLs.

How service bindings work

A service binding in .NET Aspire involves two components: a service representing an external resource your app requires (for example, a database, message queue, or API), and a binding that establishes a connection between your app and the service and provides necessary information.

.NET Aspire supports two service binding types: implicit, automatically created based on specified launch profiles defining app behavior in different environments, and explicit, manually created using WithEndpoint.

Upon creating a binding, whether implicit or explicit, .NET Aspire launches a lightweight reverse proxy on a specified port, handling routing and load balancing for requests from your app to the service. The proxy is a .NET Aspire implementation detail, requiring no configuration or management concern.

To help visualize how service bindings work, consider the .NET Aspire starter templates inner-loop networking diagram:

.NET Aspire Starter Application template inner loop networking diagram.

Launch profiles

When you call AddProject, the app host looks for Properties/launchSettings.json to determine the default set of service bindings. The app host selects a specific launch profile using the following rules:

  1. An explicit WithLaunchProfile call on the IResourceBuilder<ProjectResource>.
  2. The DOTNET_LAUNCH_PROFILE environment variable. For more information, see .NET environment variables.
  3. The first launch profile defined in launchSettings.json.

Consider the following launchSettings.json file:

{
  "$schema": "http://json.schemastore.org/launchsettings.json",
    "iisSettings": {
      "windowsAuthentication": false,
      "anonymousAuthentication": true,
      "iisExpress": {
        "applicationUrl": "http://localhost:64225",
        "sslPort": 44368
      }
    },
    "profiles": {
      "http": {
        "commandName": "Project",
        "dotnetRunMessages": true,
        "launchBrowser": true,
        "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
        "applicationUrl": "http://localhost:5066",
        "environmentVariables": {
          "ASPNETCORE_ENVIRONMENT": "Development"
        }
      },
      "https": {
        "commandName": "Project",
        "dotnetRunMessages": true,
        "launchBrowser": true,
        "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
        "applicationUrl": "https://localhost:7239;http://localhost:5066",
        "environmentVariables": {
          "ASPNETCORE_ENVIRONMENT": "Development"
        }
      },
      "IIS Express": {
        "commandName": "IISExpress",
        "launchBrowser": true,
        "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
        "environmentVariables": {
          "ASPNETCORE_ENVIRONMENT": "Development"
        }
      }
    }
  }

For the remainder of this article, imagine that you've created an IDistributedApplicationBuilder assigned to a variable named builder with the CreateBuilder() API:

var builder = DistributedApplication.CreateBuilder(args);

To specify that the https launch profile should be used, select the https launch profile from launchSettings.json. The applicationUrl of that launch profile is used to create a service binding for this project. This is the equivalent of:

builder.AddProject<Projects.Networking_Frontend>("frontend")
       .WithHttpEndpoint(port: 5066)
       .WithHttpsEndpoint(port: 7239);

Important

If there's no launchSettings.json (or launch profile), there are no bindings by default.

Ports and proxies

When defining a service binding, the host port is always given to the proxy that sits in front of the service. This allows single or multiple replicas of a service to behave similarly. Additionally, all resource dependencies that use the WithReference API rely of the proxy endpoint from the environment variable.

Consider the following method chain that calls AddProject, WithHttpEndpoint, and then WithReplicas:

builder.AddProject<Projects.Networking_Frontend>("frontend")
       .WithHttpEndpoint(port: 5066)
       .WithReplicas(2);

The preceding code results in the following networking diagram:

.NET Aspire frontend app networking diagram with specific host port and two replicas.

The preceding diagram depicts the following:

  • A web browser as an entry point to the app.
  • A host port of 5066.
  • The frontend proxy sitting between the web browser and the frontend service replicas, listening on port 5066.
  • The frontend_0 frontend service replica listening on the randomly assigned port 65001.
  • The frontend_1 frontend service replica listening on the randomly assigned port 65002.

Without the call to WithReplicas, there's only one frontend service. The proxy still listens on port 5066, but the frontend service listens on a random port:

builder.AddProject<Projects.Networking_Frontend>("frontend")
       .WithHttpEndpoint(port: 5066);

There are two ports defined:

  • A host port of 5066.
  • A random proxy port that the underlying service will be bound to.

.NET Aspire frontend app networking diagram with specific host port and random port.

The preceding diagram depicts the following:

  • A web browser as an entry point to the app.
  • A host port of 5066.
  • The frontend proxy sitting between the web browser and the frontend service, listening on port 5066.
  • The frontend service listening on random port of 65001.

The underlying service is fed this port via ASPNETCORE_URLS for project resources. Other resources access to this port by specifying an environment variable on the service binding:

builder.AddNpmApp("frontend", "../NodeFrontend", "watch")
       .WithHttpEndpoint(port: 5067, env: "PORT");

The previous code makes the random port available in the PORT environment variable. The app uses this port to listen to incoming connections from the proxy. Consider the following diagram:

.NET Aspire frontend app networking diagram with specific host port and environment variable port.

The preceding diagram depicts the following:

  • A web browser as an entry point to the app.
  • A host port of 5067.
  • The frontend proxy sitting between the web browser and the frontend service, listening on port 5067.
  • The frontend service listening on an environment 65001.

Omit the host port

When you omit the host port, .NET Aspire generates a random port for both host and service port. This is useful when you want to avoid port conflicts and don't care about the host or service port. Consider the following code:

builder.AddProject<Projects.Networking_Frontend>("frontend")
       .WithHttpEndpoint();

In this scenario, both the host and service ports are random, as shown in the following diagram:

.NET Aspire frontend app networking diagram with random host port and proxy port.

The preceding diagram depicts the following:

  • A web browser as an entry point to the app.
  • A random host port of 65000.
  • The frontend proxy sitting between the web browser and the frontend service, listening on port 65000.
  • The frontend service listening on a random port of 65001.

Container ports

When you add a container resource, .NET Aspire automatically assigns a random port to the container. To specify a container port, configure the container resource with the desired port:

builder.AddContainer("frontend", "mcr.microsoft.com/dotnet/samples", "aspnetapp")
       .WithHttpEndpoint(port: 8000, targetPort: 8080);

The preceding code:

  • Creates a container resource named frontend, from the mcr.microsoft.com/dotnet/samples:aspnetapp image.
  • Binds the host to port 8000 and the container port to 8080 with the http scheme.

Consider the following diagram:

.NET Aspire frontend app networking diagram with a docker host.

Endpoint extension methods

Any resource that implements the IResourceWithEndpoints interface can use the WithEndpoint extension methods. There are several overloads of this extension, allowing you to specify the scheme, container port, host port, environment variable name, and whether the endpoint is proxied.

There's also an overload that allows you to specify a delegate to configure the endpoint. This is useful when you need to configure the endpoint based on the environment or other factors. Consider the following code:

builder.AddProject<Projects.Networking_ApiService>("apiService")
       .WithEndpoint(
            endpointName: "admin",
            callback: static endpoint =>
       {
           endpoint.Port = 17003;
           endpoint.UriScheme = "http";
           endpoint.Transport = "http";
       });

The preceding code provides a callback delegate to configure the endpoint. The endpoint is named admin and configured to use the http scheme and transport, as well as the 17003 host port. The consumer references this endpoint by name, consider the following AddHttpClient call:

builder.Services.AddHttpClient<WeatherApiClient>(
    client => client.BaseAddress = new Uri("http://_admin.apiservice"));

The Uri is constructed using the admin endpoint name prefixed with the _ sentinel. This is a convention to indicate that the admin segment is the endpoint name belonging to the apiservice service. For more information, see .NET Aspire service discovery.