.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, endpoints, 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 endpoints, environment variables, and launch settings for your app.
  • Kestrel configuration: Kestrel configuration allows you to specify the endpoints that the Kestrel web server listens on. You can configure Kestrel endpoints in your app settings, and .NET Aspire automatically uses these settings to create endpoints.
  • Endpoints/Endpoint configurations: Endpoints are the connections between your app and the services it depends on, such as databases, message queues, or APIs. Endpoints provide information such as the service name, host port, scheme, and environment variable. You can add endpoints 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 endpoints work

A service binding in .NET Aspire involves two integrations: 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 endpoints 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 endpoints. The app host selects a specific launch profile using the following rules:

  1. An explicit launchProfileName argument passed when calling AddProject.
  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",
  "profiles": {
    "http": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": false,
      "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
      "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"
      }
    }
  }
}

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 the http and https launch profiles, configure the applicationUrl values for both in the launchSettings.json file. These URLs are used to create endpoints 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.

For more information, see .NET Aspire and launch profiles.

Kestrel configured endpoints

.NET Aspire supports Kestrel endpoint configuration. For example, consider an appsettings.json file for a project that defines a Kestrel endpoint with the HTTPS scheme and port 5271:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "Kestrel": {
    "Endpoints": {
      "Https": {
        "Url": "https://*:5271"
      }
    }
  }
}

The preceding configuration specifies an Https endpoint. The Url property is set to https://*:5271, which means the endpoint listens on all interfaces on port 5271. For more information, see Configure endpoints for the ASP.NET Core Kestrel web server.

With the Kestrel endpoint configured, the project should remove any configured applicationUrl from the launchSettings.json file.

Note

If the applicationUrl is present in the launchSettings.json file and the Kestrel endpoint is configured, the app host will throw an exception.

When you add a project resource, there's an overload that lets you specify that the Kestrel endpoint should be used instead of the launchSettings.json file:

builder.AddProject<Projects.Networking_ApiService>(
    name: "apiservice",
    configure: static project =>
    {
        project.ExcludeLaunchProfile = true;
        project.ExcludeKestrelEndpoints = false;
    })
    .WithHttpsEndpoint();

For more information, see AddProject.

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.

Tip

To avoid an endpoint being proxied, set the IsProxied property to false when calling the WithEndpoint extension method. For more information, see Endpoint extensions: additional considerations.

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.
  • Exposes an http endpoint by binding the host to port 8000 and mapping it to the container's port 8080.

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.

Additional considerations

When calling the WithEndpoint extension method, the callback overload exposes the raw EndpointAnnotation, which allows the consumer to customize many aspects of the endpoint.

The AllocatedEndpoint property allows you to get or set the endpoint for a service. The IsExternal and IsProxied properties determine how the endpoint is managed and exposed: IsExternal decides if it should be publicly accessible, while IsProxied ensures DCP manages it, allowing for internal port differences and replication.

Tip

If you're hosting an external executable that runs its own proxy and encounters port binding issues due to DCP already binding the port, try setting the IsProxied property to false. This prevents DCP from managing the proxy, allowing your executable to bind the port successfully.

The Name property identifies the service, whereas the Port and TargetPort properties specify the desired and listening ports, respectively.

For network communication, the Protocol property supports TCP and UDP, with potential for more in the future, and the Transport property indicates the transport protocol (HTTP, HTTP2, HTTP3). Lastly, if the service is URI-addressable, the UriScheme property provides the URI scheme for constructing the service URI.

For more information, see the available properties of the EndpointAnnotation properties.

Endpoint filtering

All .NET Aspire project resource endpoints follow a set of default heuristics. Some endpoints are included in ASPNETCORE_URLS at runtime, some are published as HTTP/HTTPS_PORTS, and some configurations are resolved from Kestrel configuration. Regardless of the default behavior, you can filter the endpoints that are included in environment variables by using the WithEndpointsInEnvironment extension method:

builder.AddProject<Projects.Networking_ApiService>("apiservice")
    .WithHttpsEndpoint() // Adds a default "https" endpoint
    .WithHttpsEndpoint(port: 19227, name: "admin")
    .WithEndpointsInEnvironment(
        filter: static endpoint =>
        {
            return endpoint.Name is not "admin";
        });

The preceding code adds a default HTTPS endpoint, as well as an admin endpoint on port 19227. However, the admin endpoint is excluded from the environment variables. This is useful when you want to expose an endpoint for internal use only.