Guide for deploying Duende IdentityServer to production, covering reverse proxy configuration, data protection, health checks, distributed caching, multi-instance deployment, OpenTelemetry integration, logging, and common deployment pitfalls.
Installs into .claude/skills of the current project.
Are you the author of Identityserver Deployment?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/duendesoftware-identityserver-deployment)
---
name: identityserver-deployment
description: "Guide for deploying Duende IdentityServer to production, covering reverse proxy configuration, data protection, health checks, distributed caching, multi-instance deployment, OpenTelemetry integration, logging, and common deployment pitfalls."
invocable: false
---
# IdentityServer Deployment, Proxies, and Production Readiness
## When to Use This Skill
- Deploying IdentityServer behind a reverse proxy or load balancer
- Configuring ASP.NET Core Data Protection for production persistence
- Implementing health checks for monitoring IdentityServer instances
- Setting up distributed caching for multi-instance deployments
- Configuring OpenTelemetry for metrics, traces, and logs
- Troubleshooting common deployment issues (HTTPS downgrade, cookie problems, key rotation failures)
- Understanding the difference between Data Protection keys and IdentityServer signing keys
- Setting up logging and events for production monitoring
Docs: https://docs.duendesoftware.com/identityserver/deployment
## Deployment Architecture
IdentityServer is ASP.NET Core middleware. It can be hosted with the same diversity of technology as any ASP.NET Core application:
- **Hosting**: On-premises, cloud (Azure, AWS, GCP), containers, Kubernetes
- **Web servers**: Kestrel, IIS, Nginx, Apache
- **Artifacts**: Files, containers (no Dockerfile needed with `dotnet publish /t:PublishContainer`)
- **Scaling**: Horizontal with load balancers; requires shared state for multi-instance
## Reverse Proxy and Load Balancer Configuration
### The Problem
When IdentityServer runs behind a proxy that terminates TLS or changes the originating IP, the middleware sees incorrect request information. This causes:
- HTTPS requests downgraded to HTTP
- HTTP issuer published in `.well-known/openid-configuration` instead of HTTPS
- Incorrect host names in discovery document or redirects
- Cookies missing the `Secure` attribute (breaks `SameSite` behavior)
### Solution: ForwardedHeaders Middleware
Most proxies set `X-Forwarded-For` and `X-Forwarded-Proto` headers. Configure ASP.NET Core to read them.
#### Option 1: Environment Variable (Simplest)
Set `ASPNETCORE_FORWARDEDHEADERS_ENABLED=true`. This automatically adds the middleware and accepts forwarded headers from any single proxy. Best for cloud-hosted environments and Kubernetes.
#### Option 2: Explicit Configuration (More Control)
```csharp
// Program.cs
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedHost |
ForwardedHeaders.XForwardedProto;
// Add the IP address of your known proxy
options.KnownProxies.Add(IPAddress.Parse("203.0.113.42"));
// Or use a network range
// var network = new IPNetwork(IPAddress.Parse("198.51.100.0"), 24);
// options.KnownNetworks.Add(network);
// Number of proxies in front of the app
options.ForwardLimit = 1;
});
```
**Important**: The ForwardedHeaders middleware must run **early** in the pipeline, before IdentityServer middleware and ASP.NET authentication middleware.
### Default KnownNetworks
By default, `KnownNetworks` and `KnownProxies` support localhost (`127.0.0.1/8` and `::1`). This is useful for local development or when the proxy and .NET host are on the same machine. In production, configure the actual proxy addresses.
## ASP.NET Core Data Protection
> When answering Data Protection questions, **explicitly cite the general documentation** (<https://docs.duendesoftware.com/general/data-protection/>) and state that Data Protection is a cross-cutting concern for *all* Duende SDKs (IdentityServer, BFF, etc.).
> **Cross-cutting concern:** Data protection is critical for all Duende products — both IdentityServer and BFF. See [ASP.NET Core Data Protection](https://docs.duendesoftware.com/general/data-protection/) for comprehensive guidance covering all Duende SDKs.
### Why It Matters
Data Protection is critical for IdentityServer. It encrypts and signs sensitive data including:
- Signing keys at rest (when automatic key management is used)
- Persisted grants at rest
- Server-side session data at rest
- State parameters for external OIDC providers
- UI message payloads (logout context, error context)
- Authentication session cookies
- Anti-forgery tokens
### Production Configuration
```csharp
// Program.cs
builder.Services.AddDataProtection()
// Choose a persistence method
.PersistKeysToFoo() // PersistKeysToFileSystem, PersistKeysToDbContext,
// PersistKeysToAzureBlobStorage, PersistKeysToAWSSystemsManager,
// PersistKeysToStackExchangeRedis
// Choose a key protection method
.ProtectKeysWithBar() // ProtectKeysWithCertificate, ProtectKeysWithAzureKeyVault
// Set explicit application name
.SetApplicationName("My.IdentityServer");
```
### Critical Rules
1. **Always persist keys to durable storage** using a `.PersistKeysTo...()` method
2. **Ensure the storage itself is durable** — e.g., if using Redis, configure Redis persistence (RDB/AOF)
3. **Always set an explicit application name** with `.SetApplicationName()` to prevent key isolation issues
4. **Share keys across all load-balanced instances**
5. **Consider a key escrow sink** — for backup/restore of corrupted data protection keys, configure an `IXmlEncryptor`-based escrow
### Data Protection Keys vs Signing Keys
| Aspect | Data Protection Keys | IdentityServer Signing Keys |
| ------------ | -------------------------------------------------- | ------------------------------------------------------------------------- |
| Purpose | Encrypt/sign sensitive data at rest and in cookies | Sign JWT tokens (id_tokens, access tokens) |
| Cryptography | Symmetric (private key) | Asymmetric (public/private key pair) |
| Visibility | Internal to the application | Public keys published via discovery/JWKS |
| Managed by | ASP.NET Core framework | IdentityServer (automatic key management) |
| Storage | Configured via `.PersistKeysTo...()` | File system (default), EF operational store, or custom `ISigningKeyStore` |
Both are critical secrets. Losing either causes failures.
### Common Data Protection Problems
| Problem | Symptom | Solution |
| ------------------------------------------- | ------------------------------------------------------- | --------------------------------------------------------- |
| No shared keys in load-balanced environment | `CryptographicException`: key not found in key ring | Configure shared key persistence |
| Keys generated in dev included in build | Keys from wrong environment can't be read in production | Exclude `~/keys` directory from source control and builds |
| Application name mismatch | Keys from one deployment can't be read by another | Set explicit `SetApplicationName()` consistently |
| IIS lacking permissions | Ephemeral keys generated every restart | Follow Microsoft's IIS Data Protection configuration |
| .NET 6 path normalization change | Keys break between .NET versions | Always set explicit application name (reverted in .NET 7+) |
### Symptoms of Data Protection Failure
- `CryptographicException` in logs
- Error messages like "Error unprotecting key with kid {Signing Key ID}"
- "The key {Data Protection Key ID} was not found in the key ring"
- Automatic signing key management fails silently
## IdentityServer Data Stores for Multi-Instance
### Configuration Data
For multi-instance deployments, configuration data must be shared:
| Scenario | Recommendation |
| ----------------------------- | --------------------------------------------------------------------- |
| Rarely changing configuration | In-memory stores loaded from config files (with redeploy for changes) |
| Dynamic configuration (SaaS) | Database via EF Core stores or custom stores |
### Operational Data
Operational data must always be shared in multi-instance deployments:
- Authorization codes, tokens, consent — via persisted grant store
- Signing keys — via `ISigningKeyStore` (EF operational store or custom)
- Server-side sessions — via `IServerSideSessionStore`
Use Entity Framework Core or a persistent cache like Redis.
### High Availability and Multi-Region
- Keep the IdentityServer runtime **stateless**; runtime state (grants, consent, automatic signing keys, sessions) lives in external stores. In-memory config is fine if every instance runs the same build.
- Configuration data (clients/resources) tolerates delayed replication, but disabling a client or rotating a secret only takes effect once caches/replicas catch up. Operational data must be visible to every instance almost immediately, or codes/refresh tokens fail with `invalid_grant`.
- **Consistent issuer**: `iss` is derived from the request host by default. Every instance/region must resolve the same issuer for a given request (same public host name and path base from the proxy; with a single issuer you may fix `IssuerUri`). With Multi-Issuer or Spaces the issuer legitimately varies per host/space, but must still resolve identically on every instance.
- Share signing keys (shared key store) and Data Protection keys (with the same explicit application name) across all nodes/regions; synchronize clocks.
- Active-active needs: consistent issuer, operational store visibility across regions, shared signing + DP keys, synchronized clocks. If synchronous consistency across regions isn't achievable, prefer active-passive/regional failover with defined RPO/RTO over silently accepting eventual consistency for operational data.
- User accounts live outside IdentityServer's stores; plan their replication separately.
## Distributed Caching
Some optional features require ASP.NET Core's `IDistributedCache`:
| Feature | Why It Needs Distributed Cache |
| ----------------------------- | ------------------------------------------------------------ |
| OIDC state data formatter | Stores external provider state server-side instead of in URL |
| JWT replay cache | Prevents JWT client credentials replay |
| Device flow throttling | Rate-limits polling across instances |
| Authorization parameter store | Stores PAR request data |
Configure a distributed cache for multi-instance deployments:
```csharp
// Program.cs — Example using Redis
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = "localhost:6379";
});
```
## Health Checks
Use ASP.NET Core health checks so load balancers/orchestrators can tell whether IdentityServer is healthy. The docs no longer recommend custom `IHealthCheck` classes that invoke the discovery/JWKS endpoint handlers; use a **liveness** probe (middleware is up, no dependency checks) plus a **readiness** probe (tagged dependency checks).
### Liveness
```csharp
// Program.cs
builder.Services.AddHealthChecks();
var app = builder.Build();
app.MapHealthChecks("/health/live");
```
### Liveness + Readiness (database dependencies)
```csharp
// Program.cs
builder.Services.AddHealthChecks()
.AddDbContextCheck<ConfigurationDbContext>(tags: ["db", "ready"])
.AddDbContextCheck<PersistedGrantDbContext>(tags: ["db", "ready"]);
app.MapHealthChecks("/health/live", new() { Predicate = _ => false }); // runs no checks: only proves the pipeline responds
app.MapHealthChecks("/health/ready", new() { Predicate = x => x.Tags.Contains("ready") });
```
Use tags to build multiple endpoints that check different aspects (config store, signing key store, operational store).
### Docker
```dockerfile
HEALTHCHECK CMD curl --fail http://localhost:5000/health/live || exit 1
```
## OpenTelemetry Integration
IdentityServer emits traces, metrics, and logs via the .NET OpenTelemetry SDK (added in v6.1, expanded in v7.0).
### Setup
```bash
dotnet add package OpenTelemetry
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
```
```csharp
// Program.cs
using OpenTelemetry.Resources;
// Add OpenTelemetry logging to correlate logs with traces
builder.Logging.AddOpenTelemetry();
var openTelemetry = builder.Services.AddOpenTelemetry();
openTelemetry.ConfigureResource(r => r
.AddService(builder.Environment.ApplicationName));
openTelemetry.WithMetrics(m => m
.AddMeter("Duende.IdentityServer") // Telemetry.ServiceName == "Duende.IdentityServer"
.AddPrometheusExporter());
openTelemetry.WithTracing(t => t
.AddSource(IdentityServerConstants.Tracing.Basic)
.AddSource(IdentityServerConstants.Tracing.Cache)
.AddSource(IdentityServerConstants.Tracing.Services)
.AddSource(IdentityServerConstants.Tracing.Stores)
.AddSource(IdentityServerConstants.Tracing.Validation)
.AddAspNetCoreInstrumentation()
.AddConsoleExporter());
// Add Prometheus scraping endpoint
app.UseOpenTelemetryPrometheusScrapingEndpoint();
```
### Tracing Sources
| Source | What It Traces |
| -------------------------------------------- | --------------------------------------------------------------- |
| `IdentityServerConstants.Tracing.Basic` | High-level request processing (validators, response generators) |
| `IdentityServerConstants.Tracing.Cache` | Cache operations |
| `IdentityServerConstants.Tracing.Services` | Service-layer operations |
| `IdentityServerConstants.Tracing.Stores` | Store operations (database calls) |
| `IdentityServerConstants.Tracing.Validation` | Detailed validation operations |
In production, you may want only `Basic` tracing. Use all sources during development and troubleshooting.
### Key Metrics (v7.0+)
The meter name is `Duende.IdentityServer` (accessible via `Telemetry.ServiceName`).
| Metric | Counter Name | Description |
| --------------- | --------------------------------------- | ------------------------------------------------ |
| Operations | `tokenservice.operation` | Aggregated success/failure/internal_error counts |
| Active Requests | `active_requests` | Current requests being processed by endpoints |
| Token Issuance | `tokenservice.token_issued` | Successful/failed token issuance attempts |
| Client Auth | `tokenservice.client.secret_validation` | Client authentication success/failure |
| Introspection | `tokenservice.introspection` | Token introspection counts |
| Revocation | `tokenservice.revocation` | Token revocation counts |
### UI Metrics (From Quickstart)
| Metric | Counter Name | Tags |
| ----------- | ------------------------- | --------------------------------------- |
| User Login | `tokenservice.user_login` | client, idp, error |
| User Logout | `user_logout` | idp |
| Consent | `tokenservice.consent` | client, scope, consent (granted/denied) |
## Logging
IdentityServer uses ASP.NET Core's standard `ILogger`. Logs are written under the `Duende.IdentityServer` category.
### Log Levels
| Level | Usage |
| ------------- | --------------------------------------------------- |
| `Trace` | Sensitive data (tokens); never enable in production |
| `Debug` | Internal flow and decisions; short-term debugging |
| `Information` | General application flow; long-term value |
| `Warning` | Abnormal or unexpected events |
| `Error` | Failed validation, unhandled exceptions |
| `Critical` | Missing store implementations, invalid key material |
### Configuration
```json
{
"Logging": {
"LogLevel": {
"Default": "Warning",
"Microsoft.AspNetCore": "Warning",
"Duende.IdentityServer": "Warning"
}
}
}
```
**Production appsettings (e.g. `appsettings.Production.json`): set `Default` and `Duende.IdentityServer` to `Warning` (or higher).** Raise `Duende.IdentityServer` to `Information` only temporarily when troubleshooting. `Trace` and `Debug` expose sensitive data such as tokens and must never be used in production.
### Filtering Exceptions
```csharp
builder.Services.AddIdentityServer(options =>
{
options.Logging.UnhandledExceptionLoggingFilter = (ctx, ex) =>
{
// Return false to suppress, true to log
if (ctx.RequestAborted.IsCancellationRequested && ex is OperationCanceledException)
return false; // Already the default
return true;
};
});
```
### OpenTelemetry Log Correlation
Logs written to `ILogger` in .NET 8+ can be exported to OpenTelemetry traces. Add `builder.Logging.AddOpenTelemetry()` to correlate logs with trace IDs.
## Events System
Events provide higher-level structured data about operations, suitable for APM integration.
### Enabling Events
```csharp
builder.Services.AddIdentityServer(options =>
{
options.Events.RaiseSuccessEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
});
```
### Raising Events
```csharp
public async Task<IActionResult> Login(LoginInputModel model)
{
if (_users.ValidateCredentials(model.Username, model.Password))
{
var user = _users.FindByUsername(model.Username);
await _events.RaiseAsync(
new UserLoginSuccessEvent(user.Username, user.SubjectId, user.Username));
}
else
{
await _events.RaiseAsync(
new UserLoginFailureEvent(model.Username, "invalid credentials"));
}
}
```
### Custom Event Sink
```csharp
public class SeqEventSink : IEventSink
{
private readonly Logger _log;
public SeqEventSink()
{
_log = new LoggerConfiguration()
.WriteTo.Seq("http://localhost:5341")
.CreateLogger();
}
public Task PersistAsync(Event evt)
{
if (evt.EventType == EventTypes.Success ||
evt.EventType == EventTypes.Information)
{
_log.Information("{Name} ({Id}), Details: {@details}",
evt.Name, evt.Id, evt);
}
else
{
_log.Error("{Name} ({Id}), Details: {@details}",
evt.Name, evt.Id, evt);
}
return Task.CompletedTask;
}
}
```
Register it: `builder.Services.AddTransient<IEventSink, SeqEventSink>();`
> **A custom `IEventSink` replaces the default sink.** `IEventService` sends each event to one sink, so events are no longer forwarded to the standard ASP.NET Core logger unless your custom sink does that too. If you still want events in `ILogger` output, forward to it in the sink (inject `ILogger<SeqEventSink>`) or fan out to both. Also account for destination failures and restrict write/delete access to the audit store.
Events work well with structured logging stores like ELK, Seq, or Splunk.
## Rate Limiting
Duende IdentityServer has **no built-in rate limiting**. Assess it for public-facing or multi-tenant deployments. Three combinable approaches:
### (a) Network Layer (first line of defense)
Reverse proxy / gateway (nginx, Azure Application Gateway, AWS API Gateway, Cloudflare). Partitions only by IP/path — coarse, but stops most volumetric abuse before it reaches the app.
### (b) ASP.NET Core Rate Limiting Middleware
Register **before** `app.UseIdentityServer()`:
```csharp
app.UseRateLimiter();
app.UseIdentityServer();
```
**Critical caveat:** IdentityServer matches its protocol endpoints (`/connect/authorize`, `/connect/token`, …) with its **own middleware, NOT ASP.NET Core endpoint routing**. You therefore **cannot attach a named per-endpoint policy** to protocol endpoints — only the **GLOBAL limiter** applies to them.
- Approximate per-endpoint limits by **partitioning the global limiter on `context.Request.Path`**.
- Named policies (`RequireRateLimiting("...")`) still work on **your own routed Razor Pages** (login/consent).
- For the token endpoint, prefer returning a **JSON error + `Retry-After` header** rather than an HTML 429.
```csharp
builder.Services.AddRateLimiter(options =>
{
// Global limiter — the ONLY limiter that applies to protocol endpoints
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
{
var ip = context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
// Partition on path to approximate per-endpoint limits
return RateLimitPartition.GetSlidingWindowLimiter(
partitionKey: $"{ip}:{context.Request.Path}",
factory: _ => new SlidingWindowRateLimiterOptions
{
PermitLimit = 20,
Window = TimeSpan.FromMinutes(1),
SegmentsPerWindow = 4
});
});
});
```
### (c) Identity-Aware Custom Validator
Implement `ICustomTokenRequestValidator` — it runs **after** token request validation, so `ClientId`/user are known:
```csharp
public class RateLimitingTokenRequestValidator : ICustomTokenRequestValidator
{
// v8 added the CancellationToken parameter to this interface
public Task ValidateAsync(CustomTokenRequestValidationContext context, CancellationToken ct)
{
var clientId = context.Result.ValidatedRequest.ClientId;
if (IsOverLimit(clientId))
{
context.Result.IsError = true;
context.Result.Error = "rate_limited";
context.Result.ErrorDescription = "Too many requests";
}
return Task.CompletedTask;
}
}
// idsvrBuilder.AddCustomTokenRequestValidator<RateLimitingTokenRequestValidator>();
```
**Note:** It runs **after** client authentication, secret validation, and DB lookups — so pair it with a coarser layer (a or b) to shed load earlier.
## Production Readiness Checklist
| Item | Status | Notes |
| ----------------------------------------------------- | -------------------------------------- | --------------------------------------- |
| Data Protection keys persisted to durable storage | Required | `.PersistKeysTo...()` |
| Data Protection keys shared across instances | Required for multi-instance | Same storage for all instances |
| Explicit application name set | Required | `.SetApplicationName("My.IdentityServer")` |
| ForwardedHeaders configured (if behind proxy) | Required | Match your proxy's headers |
| Operational store configured with durable persistence | Required | EF Core or custom store |
| Token cleanup enabled | Recommended | `EnableTokenCleanup = true` |
| Configuration store cache enabled | Recommended | `AddConfigurationStoreCache()` |
| Distributed cache configured (if multi-instance) | Recommended | Redis, SQL, etc. |
| Health checks implemented | Recommended | `/health/live` + tagged `/health/ready` |
| OpenTelemetry configured | Recommended | Metrics + traces for monitoring |
| Events enabled | Recommended | For auditing and APM |
| Signing key store uses durable storage | Required for multi-instance | EF operational store or custom |
| Logging level set to Warning+ for production | Recommended | Avoid log bloat |
| `~/keys` directory excluded from source control | Required if using file-based key store | Prevent dev keys in production |
| HTTPS + ForwardedHeaders configured before IdentityServer | Required if behind proxy | Discovery must publish HTTPS issuer |
| Signing keys shared by all instances + rotation plan | Required for multi-instance | Automatic Key Management where available |
| DB schema changes applied before new app version starts | Required | Plus operational-store cleanup enabled |
| Same operational data / signing keys / DP keys / caches per instance | Required for multi-instance | Every instance shares all shared state |
| CORS allows only required client origins | Required | Watch middleware order |
| Token + session lifetimes match threat model | Recommended | Tune per deployment |
| Rate limiting assessed | Recommended | Public / multi-tenant deployments |
## Common Anti-Patterns
- ❌ Deploying without configuring ForwardedHeaders behind a reverse proxy
- ✅ Always configure ForwardedHeaders when behind a proxy; test by checking the discovery document's issuer URL
- ❌ Using default (ephemeral) Data Protection keys in production
- ✅ Always persist keys to durable, shared storage with `.PersistKeysTo...()`
- ❌ Not setting `SetApplicationName()` causing key isolation between deployments
- ✅ Always set an explicit, consistent application name
- ❌ Using file-system signing key store in containerized/multi-instance deployments
- ✅ Use EF operational store or a shared `ISigningKeyStore` implementation
- ❌ Enabling `Trace` or `Debug` logging in production — exposes tokens and sensitive data
- ✅ Use `Warning` level in production; use `Information` temporarily for troubleshooting
- ❌ Not enabling token cleanup — database grows indefinitely
- ✅ Enable `EnableTokenCleanup = true` and configure appropriate intervals
## Common Pitfalls
1. **Discovery document shows HTTP issuer**: The most common deployment issue. Always configure ForwardedHeaders or the `ASPNETCORE_FORWARDEDHEADERS_ENABLED` environment variable when behind a TLS-terminating proxy.
2. **CryptographicException on startup**: Usually means Data Protection keys from one environment are being used in another. Check that keys are persisted correctly and the application name is consistent.
3. **Signing keys not shared across instances**: The default file-system key store is per-instance. Use `AddOperationalStore()` which includes `ISigningKeyStore`, or configure a custom shared store.
4. **Redis losing Data Protection keys on restart**: If using `PersistKeysToStackExchangeRedis`, configure Redis with persistence (RDB snapshots or AOF) to survive restarts.
5. **IIS Data Protection permissions**: IIS may lack permissions to persist Data Protection keys. Follow Microsoft's IIS-specific Data Protection documentation.
6. **Multiple proxies in chain**: If you have more than one proxy, set `ForwardLimit` to match the number of proxies, and add all proxy addresses to `KnownProxies` or `KnownNetworks`.
7. **Cookie SameSite failures behind proxy**: If the proxy strips HTTPS, cookies won't get the `Secure` attribute, causing `SameSite=None` cookies to be rejected by browsers. Fix the proxy configuration first.
8. **OpenTelemetry trace source selection**: In production, subscribing to all trace sources (`Stores`, `Validation`, etc.) can generate excessive trace data. Start with `Basic` and add more sources as needed for troubleshooting.
9. **v8 license key format / runtime enforcement**: The v8 license key is a signed JWT with a `kid` header. A v7-format key still runs v8 core, but a v8 key fails on v7/earlier or the BFF runtime with `IDX10503: ... Token does not have a kid.` v8 also **throws at startup** when a configured license lacks the entitlement for Server-Side Sessions, Automatic Key Management, or SAML — run lower environments with the production key so gaps surface before production.
---
## Related Skills
- `identityserver-hosting-setup` — DI registration and middleware pipeline
- `identityserver-data-storage` — EF Core stores, migrations, token cleanup
- `identityserver-aspire` — orchestrating IdentityServer in Aspire AppHost