Modern cloud-native developer environments are fantastic. Frameworks like .NET Aspire have revolutionized local development by providing a unified developer dashboard, automatic service discovery, and OTLP telemetry collection.
But what happens when it's time to deploy your microservice topology on-premises?
If your target is IIS on Windows Server or a systemd service on Linux, you've likely realized that deploying the standard .NET Aspire stack on-prem is a complex puzzle. There is no native hosting model for IIS, gRPC telemetry port mapping is fragile, and the dashboard's constant WebSocket connections can consume excessive resources on on-premises virtual machines.
Enter AppPipe.Hosting—a lightweight, developer-friendly NuGet package designed specifically to bring the best features of .NET Aspire to your on-premises environments.
The Problem: On-Premises Microservice Orchestration is Hard
While cloud platforms have native orchestration (like Kubernetes or ECS), traditional Windows and Linux environments still host a massive volume of enterprise applications. When running microservices on IIS or Linux servers, developers face three major friction points:
- Port Conflicts & Service Discovery: Dynamically assigning ports to multiple microservices in IIS or systemd and injecting them into dependent services is tedious.
- Telemetry Aggregation: Running an OpenTelemetry Collector just to aggregate traces, logs, and metrics for a small on-prem cluster is heavy and complex to configure.
- Resource Exhaustion: Standard Blazor Interactive Server dashboards maintain constant WebSockets and high memory usage, which quickly drains limited hosting environments.
The Solution: AppPipe
AppPipe is a lightweight alternative that integrates a routing gateway, an in-memory telemetry store, and a visual dashboard directly into a single library.
graph TD
Client(Browser/Client) -->|HTTP| Gateway
subgraph User's Application Space
Backend1[Backend Microservice A]
Backend2[Backend Microservice B]
end
subgraph AppPipe.Hosting NuGet Package
Gateway[AppPipe Gateway / YARP]
TelemetryPort[Gateway Telemetry Port]
Store[In-Memory Telemetry Store]
Dashboard[Blazor Dashboard UI]
TelemetryPort --> Store
Store --> Dashboard
end
Gateway -->|Service Discovery Routing| Backend1
Gateway -->|Service Discovery Routing| Backend2
Backend1 -->|OTLP gRPC| TelemetryPort
Backend2 -->|OTLP gRPC| TelemetryPort
Key Capabilities:
- ⚡ Service Discovery & Routing via YARP: Under the hood, AppPipe uses Microsoft's YARP (Yet Another Reverse Proxy). It serves as a single entry point for your client, proxying traffic to backend microservices automatically using clean, virtual paths.
- 📊 OTLP Telemetry Collection: AppPipe exposes a dedicated HTTP/2 Kestrel endpoint that acts as a local OTLP collector. Your microservices export standard traces, logs, and metrics directly to the AppPipe Gateway, which parses and displays them in the dashboard.
- 🖥️ Gorgeous Blazor Dashboard: View traces as waterfall flamegraphs, inspect console logs, and monitor metrics over time in a responsive Light/Dark theme dashboard.
- ⚡ Dual Render Modes: Run the dashboard in full WebSocket InteractiveServer mode for real-time updates during development, or swap to Server-Side Rendering (SSR) mode in production to drastically reduce CPU and memory consumption.
-
🚀 Built-in On-Prem Deployment Pipeline: Using
ModularPipelines, AppPipe provides automatic scripts to publish your services, spin up dedicated IIS Application Pools, configure Windows sub-applications, and inject service endpoints and telemetry tokens safely.
How Easy Is It to Use?
Setting up AppPipe in your orchestration host project takes just a few lines of C#:
using AppPipe.Hosting;
var builder = AppPipeHostingApp.CreateBuilder(args);
// Define a backend worker microservice using compile-safe generated constant
var backend = builder.AddProject(AppPipeProjects.BackendWorker);
// Or register directly using raw string name:
// var backend = builder.AddProject("BackendWorker");
// Define a frontend API and declare its reference to BackendWorker
var frontend = builder.AddProject(AppPipeProjects.FrontendApi)
.WithReference(backend); // Injects service discovery variables automatically
var app = builder.Build();
// Run the host using AppPipeDevHostRunner
var runner = new AppPipeDevHostRunner(app);
await runner.RunAsync();
Inside your microservices, register the standard OpenTelemetry exporter:
builder.Services.AddOpenTelemetry()
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddOtlpExporter()) // Automatically routes to AppPipe telemetry port
.WithMetrics(metrics => metrics
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddOtlpExporter());
💾 Customizing the Telemetry Database (SQLite, SQL Server, ClickHouse, etc.)
By default, AppPipe retains telemetry in a circular in-memory buffer (InMemoryTelemetryStore). If you are running in production or want persistent diagnostic logs, you can easily plug in your own database.
AppPipe defines a simple dependency injection contract for storage using ITelemetryStore:
namespace AppPipe.Gateway.Services;
public interface ITelemetryStore
{
ConcurrentDictionary<string, ParsedTrace> Traces { get; }
ConcurrentQueue<ParsedLog> Logs { get; }
ConcurrentQueue<ExportMetricsServiceRequest> Metrics { get; }
void AddTrace(ExportTraceServiceRequest request);
void AddLog(ExportLogsServiceRequest request);
void AddMetric(ExportMetricsServiceRequest metric);
event Action? OnTelemetryReceived;
}
Implementing and Configuring a Custom Database
-
Write Your Implementation: Create a class implementing
ITelemetryStorethat persists traces, logs, and metrics to your chosen database (e.g. PostgreSQL, SQLite, or ClickHouse):
public class SqliteTelemetryStore : ITelemetryStore
{
// Implement parsing, writing, and querying logic here...
public void AddTrace(ExportTraceServiceRequest request)
{
// Save to SQLite DB
OnTelemetryReceived?.Invoke();
}
// ...
}
- Register It in the DI Container: Register your custom telemetry store with the gateway builder during startup:
var builder = AppPipeHostingApp.CreateBuilder(args);
// Register custom Telemetry Store on the Gateway's DI container:
builder.ConfigureGateway(gatewayBuilder =>
{
gatewayBuilder.Services.AddSingleton<ITelemetryStore, SqliteTelemetryStore>();
});
var app = builder.Build();
// Run the host using DevHostRunner
var runner = new DevHostRunner(app);
await runner.RunAsync();
AppPipe will automatically bypass the default in-memory storage and route all parsed incoming OTLP exports to your database store instead!
Built for the Realities of On-Prem Hosting
When deploying to production IIS environments, AppPipe features specific optimizations to ensure maximum reliability and low overhead:
1. IIS Token Overwrite Filter
IIS integration validates incoming requests using an MS-ASPNETCORE-TOKEN pairing token. Since outgoing HTTP calls from your microservices carry mismatched pairing tokens from different AppPools, IIS natively rejects telemetry exports. AppPipe implements a custom startup filter at index 0 that overrides these headers for loopback connections on the telemetry port, allowing OTLP traffic to flow without compromising security.
2. High-Performance SSR Mode
To prevent Blazor Interactive Server from spawning idle WebSockets and consuming RAM on your servers, setting Dashboard:UseWebSockets to false configures the dashboard in pure SSR mode. All refreshes, trace waterfalls, and searches are handled cleanly with base-relative URLs that work perfectly under IIS sub-applications (e.g. /your-sub-app/).
3. In-Memory Cap
AppPipe retains up to 200 traces and metrics in-memory, ensuring you have enough diagnostic history to investigate issues without causing memory leak overhead.
4. Granular Service & AppPool Customization
Deploying in enterprise environments means configuring security and application lifecycles. AppPipe now provides a fluent C# API to configure:
- Custom AppPools: Run services under dedicated resource pools.
-
Service Identities & Credentials: Run Windows Services or IIS AppPools under custom domain user credentials or system identities (like
NetworkServiceorLocalSystem). -
SCM Startup Types & Details: Control service startup triggers (
auto,demand,disabled) and descriptions in Windows SCM. -
Hosting Models: Force IIS hosting models (e.g.
InProcessvsOutOfProcess).
🚀 Integrating AppPipe into DevOps Pipelines
In professional staging and production environments, you build your code once (generating DLLs) and deploy those pre-compiled artifacts across multiple environments. The target servers do not contain the source code or the .NET SDK.
AppPipe is built to accommodate this separation of concerns:
1. Build Once (CI)
On your build server, compile and publish your orchestrator and microservice projects:
dotnet publish samples/AppPipe.DevHost/AppPipe.DevHost.csproj -c Release -o ./publish/AppPipe.DevHost
dotnet publish samples/BackendWorker/BackendWorker.csproj -c Release -o ./publish/BackendWorker
dotnet publish samples/FrontendApi/FrontendApi.csproj -c Release -o ./publish/FrontendApi
Package and upload the ./publish directory as a build artifact.
2. Deploy Anywhere (CD)
On the target machine, download your pre-compiled files and invoke the orchestrator with the --prepublished-dir option:
dotnet C:\inetpub\apps\AppPipe\AppPipe.DevHost.dll --deploy iis --prepublished-dir C:\inetpub\apps\AppPipe
This flag tells the orchestrator to bypass .csproj checks and local compilation steps entirely, deploying direct to IIS or SCM from your pre-compiled DLLs.
3. Injecting Environment Configs & Secrets
To configure environment-specific settings (like AppPool names or site directories) and environment secrets (like service passwords), bind the topology definition to standard .NET Configuration:
var config = new ConfigurationBuilder()
.AddJsonFile("appsettings.json", optional: true)
.AddEnvironmentVariables(prefix: "APPIPE__")
.AddCommandLine(args)
.Build();
var appPool = config["BackendWorker:AppPoolName"] ?? "ProdPool";
var password = config["BackendWorker:ServicePassword"];
builder.AddProject("BackendWorker")
.WithAppPool(appPool)
.WithServiceAccount(@"DOMAIN\ServiceAccount")
.WithServicePassword(password);
4. Customizing the Dashboard Application Name & Details
You can customize the dashboard application name, its IIS AppPool, its sub-path, and its Windows Service details in exactly the same way as your other microservices:
// Define a custom name for the dashboard host
builder.HostProject = new AppPipeHostingProjectResource("EnterpriseDashboard", "");
// Configure the dashboard fluently
builder.HostProject.WithEndpoint(7001)
.WithIISSite("Default Web Site")
.WithAppPath("/") // Deployed at the root path '/' of the site/proxy
.WithAppPool("EnterpriseDashboardPool")
.WithServiceDisplayName("Enterprise Telemetry Gateway")
.WithServiceDescription("Aggregates telemetry from all active business microservices.");
During your CD release stage, you can inject these settings dynamically:
-
Environment Variables: Map DevOps secrets/variables as environment variables (e.g.,
APPIPE__BackendWorker__ServicePassword$\rightarrow$$(SecretPassword)). - Command Line Params: Pass them directly to the orchestrator:
dotnet AppPipe.DevHost.dll --deploy iis --prepublished-dir C:\inetpub\apps\AppPipe --BackendWorker:AppPoolName "ProdPool" --BackendWorker:ServicePassword "$(SecretPassword)"
Get Started Today
AppPipe is open-source and ready to streamline your on-premises microservices.
- GitHub Repository: SitholeWB/AppPipe.Hosting
- NuGet Package: AppPipe.Hosting
Top comments (0)