Skip to content

Hosting in ConnectSoft Templates

Purpose & Overview

Hosting in ConnectSoft Templates refers to the complete lifecycle management of applications from bootstrap through runtime to graceful shutdown. The templates provide a unified, configurable hosting strategy that supports multiple deployment environments: Docker containers, IIS on Windows, Linux services via systemd, Azure App Services, and Kubernetes.

Hosting encompasses:

  • Application Bootstrap: Host builder configuration, dependency injection, and service registration
  • Web Server Configuration: Kestrel server setup with HTTP/HTTPS endpoints, protocol support, and limits
  • Middleware Pipeline: Request processing pipeline with forwarding, routing, authentication, CORS, and observability
  • Environment Configuration: Environment-specific settings via appsettings.{Environment}.json
  • Lifecycle Management: Startup, shutdown, and graceful termination hooks
  • Multi-Platform Support: Consistent behavior across Docker, IIS, Azure, Linux, and local development

Hosting Philosophy

ConnectSoft treats hosting as a first-class concern with consistent, repeatable patterns across all applications. The template abstracts complexity into composable extension methods while maintaining full control and customization capabilities.

Architecture Overview

The hosting architecture follows a layered approach:

Hosting System
├── Entry Point (Program.cs)
│   ├── Host Builder Configuration
│   ├── Logging Setup
│   ├── Configuration Loading
│   └── Thread Pool Configuration
├── Service Registration (MicroserviceRegistrationExtensions)
│   ├── Domain & Application Services
│   ├── Infrastructure (Persistence, Messaging)
│   ├── Observability (Logging, Metrics, Tracing)
│   └── Service Models (REST, gRPC, GraphQL, SignalR)
├── Middleware Pipeline (UseMicroserviceServices)
│   ├── Forwarded Headers
│   ├── Exception Handling
│   ├── Static Files & Default Files
│   ├── Routing & CORS
│   ├── Authentication & Authorization
│   └── Endpoint Mapping
└── Runtime Execution
    ├── Kestrel Server
    ├── Health Checks
    └── Lifecycle Hooks

Entry Point and Host Builder

Program.cs Structure

The application entry point uses the traditional host builder pattern:

namespace ConnectSoft.Template.Application
{
    public static class Program
    {
        public static async Task<int> Main(string[] args)
        {
            // Configure thread pool to prevent starvation
            ConfigureThreadPool();

            IHost host = CreateHostBuilder(args).Build();
            await host.RunAsync().ConfigureAwait(false);

            return 0;
        }

        private static IHostBuilder CreateHostBuilder(string[] args)
        {
            IHostBuilder hostBuilder =
                Host.CreateDefaultBuilder(args)
                    .ConfigureLogging(logging => DefineLogging(logging))
                    .ConfigureAppConfiguration((hostBuilderContext, configurationBuilder) =>
                    {
                        DefineConfiguration(args, hostBuilderContext, configurationBuilder);
                    })
#if UseOrleans
                    .UseMicroserviceOrleans()
#endif
                    .ConfigureWebHostDefaults(webBuilder =>
                    {
                        webBuilder.UseDefaultServiceProvider((context, options) =>
                        {
                            options.ValidateScopes = false;
                            options.ValidateOnBuild = false;
                        });

                        webBuilder.CaptureStartupErrors(captureStartupErrors: false);
                        webBuilder.UseSetting(WebHostDefaults.DetailedErrorsKey, bool.TrueString);

                        DefineKestrel(webBuilder);
                        webBuilder.UseStartup<Startup>();
                    })
#if Serilog
                    .UseSerilog((hostingContext, services, configuration) =>
                        configuration
                            .WriteTo.Console()
                            .ReadFrom.Configuration(hostingContext.Configuration)
                            .ReadFrom.Services(services))
#endif
            ;

            return hostBuilder;
        }
    }
}

Thread Pool Configuration

Prevents thread starvation in high-concurrency scenarios:

private static void ConfigureThreadPool()
{
    int processorCount = Environment.ProcessorCount;

    // Calculate optimal thread pool sizes
    // For Orleans workloads, we want more threads than default
    int minWorkerThreads = Math.Max(processorCount * 2, 50);
    int minIoThreads = Math.Max(processorCount, 25);

    // Set minimum worker threads to prevent delays when pool needs to grow
    ThreadPool.SetMinThreads(minWorkerThreads, minIoThreads);
}

Configuration Loading

Configuration is loaded in priority order:

private static void DefineConfiguration(
    string[] args, 
    HostBuilderContext hostBuilderContext, 
    IConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Sources.Clear();
    configurationBuilder
        .SetBasePath(hostBuilderContext.HostingEnvironment.ContentRootPath)
        .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
        .AddJsonFile(
            $"appsettings.{hostBuilderContext.HostingEnvironment.EnvironmentName}.json",
            optional: true,
            reloadOnChange: true);

#if UseAzureAppConfigurationAsAdditionalConfigurationProvider
    IConfigurationRoot configurationRoot = configurationBuilder.Build();

    configurationBuilder.AddAzureAppConfiguration(options =>
    {
        string? azureAppConfigurationConnectionString = 
            configurationRoot.GetConnectionString("AzureAppConfiguration");
        options.Connect(azureAppConfigurationConnectionString)
            .Select("ConnectSoft.Template:*", LabelFilter.Null)
            .ConfigureRefresh(refresh =>
            {
                refresh.Register("ConnectSoft.Template:Settings:Sentinel", refreshAll: true);
                refresh.SetRefreshInterval(TimeSpan.FromMinutes(30));
            });
    });
#endif

    configurationBuilder
        .AddEnvironmentVariables()
        .AddCommandLine(args);
}

Startup Class

The Startup class separates service registration from middleware configuration:

public class Startup(IConfiguration configuration, IWebHostEnvironment webHostEnvironment)
{
    protected IConfiguration Configuration { get; private set; } = configuration;
    protected IWebHostEnvironment WebHostEnvironment { get; private set; } = webHostEnvironment;

    public virtual void ConfigureServices(IServiceCollection services)
    {
        services.ConfigureMicroserviceServices(this.Configuration, this.WebHostEnvironment);
    }

    public void Configure(
        IApplicationBuilder app, 
        IWebHostEnvironment environment, 
        ILoggerFactory loggerFactory, 
        IHostApplicationLifetime hostApplicationLifetime)
    {
        app.UseMicroserviceServices(this.Configuration, environment, loggerFactory, hostApplicationLifetime);
    }
}

Kestrel Server Configuration

Configuration via appsettings.json

Kestrel is configured declaratively through configuration:

{
  "Kestrel": {
    "Endpoints": {
      "Http": {
        "Url": "http://0.0.0.0:5000"
      },
      "Https": {
        "Url": "https://0.0.0.0:5001",
        "Certificate": {
          "Path": "/https/aspnetapp.pfx",
          "Password": "Password123!"
        }
      }
    },
    "Limits": {
      "MaxRequestBodySize": 104857600,
      "MaxConcurrentConnections": 100,
      "MaxConcurrentUpgradedConnections": 50
    },
    "AllowSynchronousIO": false
  }
}

Kestrel Setup in Program.cs

private static void DefineKestrel(IWebHostBuilder webBuilder)
{
    webBuilder.ConfigureKestrel((context, options) =>
    {
        // Bind the "Kestrel" section of appsettings.json to Kestrel server options
        IConfigurationSection kestrelSection = context.Configuration.GetRequiredSection("Kestrel");
        kestrelSection.Bind(options);

        // Set HTTP/1.1, HTTP/2, and HTTP/3 protocol support
        options.ConfigureEndpointDefaults(listenOptions =>
        {
            listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
        });
    });
}

Port Binding Considerations

Binding Purpose Use Case
0.0.0.0:5000 Listen on all interfaces Docker containers, Kubernetes pods
localhost:5000 Listen on loopback only Local development, behind reverse proxy
127.0.0.1:5000 Explicit loopback Same as localhost

HTTP/2 and gRPC Support

HTTP/2 is required for gRPC:

  • Kestrel must bind to HTTPS endpoint for browser compatibility
  • Protocol support includes HTTP/1.1, HTTP/2, and HTTP/3
  • All protocols can be enabled simultaneously

Kestrel Limits

Limit Purpose Default
MaxRequestBodySize Maximum request body size 30 MB
MaxConcurrentConnections Maximum concurrent connections Unlimited
MaxConcurrentUpgradedConnections WebSocket/upgrade connections Unlimited
AllowSynchronousIO Allow synchronous I/O false (recommended)

Service Registration

MicroserviceRegistrationExtensions

All services are registered via ConfigureMicroserviceServices:

public static IServiceCollection ConfigureMicroserviceServices(
    this IServiceCollection services, 
    IConfiguration configuration, 
    IWebHostEnvironment environment)
{
    // Options and configuration
    services.AddMicroserviceOptions(configuration);

    // Hosted services
    services.AddMicroserviceHostedServices();

    // AutoMapper
    services.AddAutoMapper();

    // Localization
    services.AddMicroserviceLocalization();

    // Domain model services
    services.AddDomainModelServices();

    // HTTP context accessor
    services.AddHttpContextAccessor();

    // Service discovery
    services.AddMicroserviceServiceDiscovery();

    // HTTP clients
    services.AddMicroserviceHttpClient();

    // Redaction
    services.AddMicroserviceRedaction(configuration, environment);

#if Serilog
    services.AddAndConfigureSerilogLogging(configuration, environment);
#endif

    // HTTP logging
    services.AddMicroserviceHttpLogging(configuration);

#if UseHangFire
    services.AddMicroserviceHangFire(configuration);
#endif

#if UseApplicationInsights
    services.AddMicroserviceAzureApplicationInsights();
#endif

#if OpenTelemetry
    services.AddMicroserviceOpenTelemetry(environment);
#endif

    // Metrics
    services.AddMicroserviceMetrics();

#if DistributedCacheRedis
    services.AddRedisCaching(configuration);
#endif

#if DistributedCacheInMemory
    services.AddInMemoryCaching();
#endif

    // Routing and rate limiting
    services.AddRouting();
    services.AddMicroserviceRateLimiting();

#if CORS
    services.AddCors();
#endif

#if ResourceMonitoring
    services.AddMicroserviceResourceMonitoring();
#endif

    // Latency telemetry
    services.AddLatencyTelemetryCollection();

#if UseRestApi
    services.AddMicroserviceProblemDetails();
    services.AddWebApiCommunication();
#endif

#if UseCoreWCF
    services.AddWcfCoreServiceModel();
#endif

#if UseMongoDb
#if Migrations
    services.AddMongoDbMigrator(configuration, typeof(MicroserviceMongoDbMigration));
#endif
    services.AddMongoDbPersistence(configuration);
#endif

#if Migrations
    services.AddMicroserviceFluentMigrator(configuration, typeof(MicroserviceMigration).Assembly);
#endif

#if UseNHibernate
    services.AddNHibernatePersistenceModel(configuration);
#endif

#if (UseNHibernate || UseMongoDb)
    services.AddPersistenceModel();
#endif

#if Swagger && (UseGrpc || UseRestApi || UseAzureFunction)
    services.AddSwagger();
#endif

#if UseGrpc
    services.AddGrpcCommunication();
#endif

#if UseAzureFunction
    services.AddAzureFunctionsCommunication();
#endif

#if HealthCheck
    services.AddMicroserviceHealthChecks(configuration);
#endif

    // FluentValidation
    services.AddFluentValidation();

#if FeatureFlags
    services.AddMicroserviceFeatureManagement();
#endif

#if UseAzureAppConfigurationAsAdditionalConfigurationProvider
    services.AddMicroserviceAzureAppConfiguration();
#endif

#if UseNServiceBus
    services.AddNServiceBus(configuration);
#endif

#if UseMassTransit
    services.AddMicroserviceMassTransit();
#endif

    // Header propagation
    services.AddMicroserviceHeaderPropagation();

#if UseAuditNet
    services.AddAuditNet();
#endif

#if UseAzureOpenAI
    services.AddMicroserviceAzureOpenAI();
#endif

#if UseOpenAI
    services.AddMicroserviceOpenAI();
#endif

#if UseMicrosoftExtensionsAI
    services.AddMicrosoftAI();
#endif

#if UseSemanticKernel
    services.AddMicroserviceSemanticKernel();
#endif

#if UseMicrosoftBotBuilder
    services.AddMicroserviceMicrosoftBotBuilder();
#endif

#if UseMCP
    services.AddMicroserviceMCPServer();
#endif

#if UseSignalR
    services.AddSignalRCommunication();
#endif

    return services;
}

Middleware Pipeline

UseMicroserviceServices

The middleware pipeline is constructed via UseMicroserviceServices:

public static IApplicationBuilder UseMicroserviceServices(
    this IApplicationBuilder application, 
    IConfiguration configuration, 
    IWebHostEnvironment environment, 
    ILoggerFactory loggerFactory, 
    IHostApplicationLifetime hostApplicationLifetime)
{
    // Register shutdown handler
    hostApplicationLifetime.ApplicationStopping.Register(OnStopping);

#if UseRestApi
    application.UseMicroserviceProblemDetails();
#endif

    // Forwarded headers (for reverse proxy support)
    application.UseForwardedHeaders();

#if Log4Net
    loggerFactory.UseLog4Net();
#endif

#if UseNHibernate
    loggerFactory.UseNHibernateLogging();
    application.RunMicroserviceFluentMigrations();
#endif

#if UseNServiceBus
    loggerFactory.UseNServiceBusLogging();
#endif

#if UseSemanticKernel
    loggerFactory.UseSemanticKernelLogging();
#endif

#if Serilog
    application.UseMicroserviceSerilogRequestLogging();
#endif

    // Exception handling
    if (environment.IsDevelopment())
    {
        application.UseDeveloperExceptionPage();
    }
    else
    {
        application.UseHsts();
    }

#if UseAzureAppConfigurationAsAdditionalConfigurationProvider
    application.UseMicroserviceAzureAppConfiguration();
#endif

    // Conversation ID for correlation
    application.UseConversationId();

    // HTTP logging
    application.UseMicroserviceHttpLogging(configuration);

    // Latency telemetry
    application.UseLatencyTelemetryCollection();

    // Static files (serves from wwwroot/)
    application.UseDefaultFiles();
    application.UseStaticFiles();

    // WebSockets
    application.UseWebSockets();

#if UseRestApi
    application.UseWebApiCommunication();
#endif

#if UseCoreWCF
    application.UseWcfCoreServiceModel();
#endif

#if CORS
    application.UseCors();
#endif

    // HTTPS redirection
    application.UseHttpsRedirection();

    // Routing
    application.UseRouting();

    // Rate limiting
    application.UseMicroserviceRateLimiter();

    // Localization
    application.UseMicroserviceLocalization();

    // Header propagation
    application.UseMicroserviceHeaderPropagation();

#if Swagger && (UseGrpc || UseRestApi || UseAzureFunction)
    application.UseMicroserviceSwagger();
#endif

#if UseHangFire
    application.UseMicroserviceHangFire();
#endif

#if OpenTelemetry
    application.UseMicroserviceOpenTelemetry();
#endif

#if UseOrleans
    application.MapOrleansDashboard();
#endif

    // Map endpoints
    application.UseEndpoints(endpoints =>
    {
        endpoints.MapEndpoints();
    });

#if UseNServiceBus
    application.ApplicationServices.StartNServiceBusEndpoint();
#endif

    // Mark warmup as complete
    hostApplicationLifetime.ApplicationStarted.Register(() =>
    {
        var gate = application.ApplicationServices.GetRequiredService<StartupWarmupGate>();
        gate.MarkReady();
    });

    return application;
}

Endpoint Mapping

Endpoints are mapped via MapEndpoints:

private static void MapEndpoints(this IEndpointRouteBuilder endpoints)
{
#if UseGrpc
    endpoints.MapGrpcServices();
#endif

#if HealthCheck
    endpoints.MapMicroserviceHealthChecks();
#endif

    // Root endpoint
    endpoints.MapGet("/", async context =>
    {
        await context.Response.WriteAsync(
            "Welcome to ConnectSoft.Template's application back-end application", 
            cancellationToken: context.RequestAborted);
    });

#if Swagger && (UseGrpc || UseRestApi || UseAzureFunction)
    endpoints.MapMicroserviceSwagger();
#endif

#if Scalar && (UseGrpc || UseRestApi || UseAzureFunction)
    endpoints.MapScalar();
#endif

#if UseMCP
    endpoints.MapMicroserviceMCPServer();
#endif

#if UseRestApi
    endpoints.MapMicroserviceControllers();
#endif

#if UseSignalR
    endpoints.MapSignalR();
#endif
}

Middleware Order

The middleware pipeline executes in a specific order:

  1. Forwarded Headers - Process reverse proxy headers first
  2. Exception Handling - Catch exceptions early
  3. Logging - Log all requests
  4. Static Files - Serve static content before routing
  5. Routing - Determine endpoint
  6. Authentication - Verify identity
  7. Authorization - Check permissions
  8. Endpoints - Execute handler

Forwarded Headers and Reverse Proxy Support

UseForwardedHeaders Configuration

Required when hosting behind reverse proxies (Nginx, IIS, ingress controllers):

application.UseForwardedHeaders();

By default, this enables: - X-Forwarded-For - Original client IP - X-Forwarded-Proto - Original request scheme (HTTP/HTTPS) - X-Forwarded-Host - Original host header

Known Proxies

For security, configure known proxy IPs:

services.Configure<ForwardedHeadersOptions>(options =>
{
    options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
    options.KnownProxies.Add(IPAddress.Parse("172.20.0.1")); // Docker bridge
    options.KnownNetworks.Add(new IPNetwork(IPAddress.Parse("10.0.0.0"), 8)); // Internal network
});

Docker Hosting

Dockerfile

Multi-stage Dockerfile for production builds:

# Base runtime image
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
WORKDIR /app
EXPOSE 8081
EXPOSE 7279

# Build stage
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR app

# Copy solution and project files
COPY ["Directory.Packages.props", "."]
COPY ["Directory.Build.props", "."]
COPY ["nuget.config", "."]
COPY ["ConnectSoft.Template.slnx", "."]
# ... copy all project files ...

# Restore dependencies
RUN dotnet restore ./ConnectSoft.Template.Application/ConnectSoft.Template.Application.csproj \
    --configfile "nuget.config"

# Copy source code
COPY ["ConnectSoft.Template/.", "./ConnectSoft.Template/"]
# ... copy all source files ...

# Publish application
FROM build AS publish
RUN dotnet publish "/app/ConnectSoft.Template.Application/ConnectSoft.Template.Application.csproj" \
    -c Release \
    -o /app/publish \
    --configfile "/app/nuget.config"

# Final runtime image
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "ConnectSoft.Template.Application.dll"]

Docker Compose

Multi-container orchestration:

services:
  connectsoft.applicationtemplate.application:
    image: ${DOCKER_REGISTRY-}connectsoft.applicationtemplate.application
    container_name: connectsoft.applicationtemplate.application
    build:
      context: .
      dockerfile: ../ConnectSoft.Template.Application/Dockerfile
    depends_on:
      - sql
      - redis
      - mongo
      - otel-collector
      - rabbitmq
    networks:
      - backend

  sql:
    image: "mcr.microsoft.com/mssql/server:2022-latest"
    container_name: sql
    ports:
      - "1433:1433"
    environment:
      - ACCEPT_EULA=y
      - MSSQL_SA_PASSWORD=ConnectSoft123!
      - MSSQL_MEMORY_LIMIT_MB=4096
    networks:
      - backend

  redis:
    image: redis
    container_name: redis
    ports:
      - "60003:60002"
    networks:
      - backend

  mongo:
    image: mongo:latest
    container_name: mongo
    ports:
      - "60001:27017"
    networks:
      - backend

  rabbitmq:
    image: rabbitmq:3-management
    container_name: rabbitmq
    ports:
      - "5672:5672"
      - "15672:15672"
    networks:
      - backend

  otel-collector:
    image: otel/opentelemetry-collector-contrib:latest
    container_name: otel-collector
    ports:
      - "4317:4317"
      - "4318:4318"
    networks:
      - backend

networks:
  backend:
    driver: bridge

volumes:
  httpscerts: {}

HTTPS Certificates in Docker

Certificates are mounted as volumes:

services:
  certgen:
    image: mcr.microsoft.com/dotnet/sdk:8.0
    container_name: certgen
    entrypoint: ["bash","-lc","dotnet dev-certs https -ep /out/aspnetapp.pfx -p 123456"]
    volumes:
      - httpscerts:/out
    restart: "no"

  connectsoft.applicationtemplate.application:
    volumes:
      - httpscerts:/https:ro

Kestrel configuration references the mounted certificate:

{
  "Kestrel": {
    "Endpoints": {
      "Https": {
        "Url": "https://0.0.0.0:5001",
        "Certificate": {
          "Path": "/https/aspnetapp.pfx",
          "Password": "123456"
        }
      }
    }
  }
}

IIS Hosting

web.config

For IIS hosting, web.config is required:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <location path="." inheritInChildApplications="false">
    <system.webServer>
      <handlers>
        <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
      </handlers>
      <aspNetCore processPath="dotnet"
                  arguments=".\ConnectSoft.Template.Application.dll"
                  stdoutLogEnabled="false"
                  stdoutLogFile=".\logs\stdout"
                  hostingModel="inprocess" />
    </system.webServer>
  </location>
</configuration>

Hosting Models

Model Configuration Performance Use Case
In-Process hostingModel="inprocess" Faster Recommended for .NET Core 3.1+
Out-of-Process hostingModel="outofprocess" Slightly slower Matches Docker/Kestrel behavior

IIS Integration

Ensure IIS integration is enabled (auto-detected when running under IIS):

// Not explicitly required in .NET 6+, but can be added for clarity
webBuilder.UseIISIntegration();

This enables: - Header forwarding - Port resolution from IIS - Graceful shutdown support

Linux Hosting with systemd

systemd Service File

Create /etc/systemd/system/connectsoft-application.service:

[Unit]
Description=ConnectSoft ConnectSoft Templates
After=network.target

[Service]
Type=notify
WorkingDirectory=/opt/connectsoft/application
ExecStart=/usr/bin/dotnet /opt/connectsoft/application/ConnectSoft.Template.Application.dll
Restart=always
RestartSec=10
SyslogIdentifier=connectsoft-application
User=dotnet
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false

[Install]
WantedBy=multi-user.target

Deployment Steps

  1. Publish the application:

    dotnet publish -c Release -o /opt/connectsoft/application
    

  2. Register and start the service:

    sudo systemctl daemon-reload
    sudo systemctl enable connectsoft-application
    sudo systemctl start connectsoft-application
    

  3. Check status:

    sudo systemctl status connectsoft-application
    

  4. View logs:

    journalctl -u connectsoft-application -f
    

Nginx Reverse Proxy

Configure Nginx to proxy to Kestrel:

server {
    listen 80;
    server_name application.example.com;

    location / {
        proxy_pass         http://localhost:5000;
        proxy_http_version 1.1;
        proxy_set_header   Upgrade $http_upgrade;
        proxy_set_header   Connection keep-alive;
        proxy_set_header   Host $host;
        proxy_set_header   X-Real-IP $remote_addr;
        proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;
    }
}

Azure App Service Hosting

Deployment Options

Method Tool Artifact
ZIP Deploy Azure CLI, Portal .zip archive
Docker Container Registry Docker image
GitHub Actions Azure Deploy Action Build artifact
Azure DevOps Azure Web App Task Build artifact

Configuration

Required App Service settings:

Setting Value Purpose
ASPNETCORE_ENVIRONMENT Production Environment name
WEBSITES_PORT 5000 Port Kestrel listens on
DOTNET_RUNNING_IN_CONTAINER false Container detection
APPINSIGHTS_INSTRUMENTATIONKEY ... Application Insights

Connection Strings

Configure via App Service Configuration:

ConnectionStrings__DefaultConnection = Server=...;Database=...;...
ConnectionStrings__Redis = redis.redis.cache.windows.net:6380,password=...

HTTPS

Azure App Service provides automatic HTTPS certificates via Let's Encrypt. Custom certificates can be bound via the portal or Azure CLI.

Application Lifecycle

IHostApplicationLifetime

The IHostApplicationLifetime interface provides lifecycle hooks:

public interface IHostApplicationLifetime
{
    CancellationToken ApplicationStarted { get; }
    CancellationToken ApplicationStopping { get; }
    CancellationToken ApplicationStopped { get; }

    void StopApplication();
}

Startup Warmup Gate

The template uses a warmup gate to prevent health checks from passing before the application is fully initialized:

hostApplicationLifetime.ApplicationStarted.Register(() =>
{
    var gate = application.ApplicationServices.GetRequiredService<StartupWarmupGate>();
    gate.MarkReady();
});

Graceful Shutdown

Shutdown handlers are registered for cleanup:

private static void OnStopping()
{
#if UseNServiceBus
    NServiceBusExtensions.StopNServiceBusEndpoint();
#endif

#if Serilog
    SerilogLoggingExtensions.CloseAndFlush();
    Thread.Sleep(1000); // Allow time for logs to flush
#endif
}

Shutdown Best Practices

  1. Stop accepting new requests: Health checks should fail during shutdown
  2. Complete in-flight requests: Allow reasonable timeout
  3. Flush telemetry: Export logs, metrics, and traces
  4. Close connections: Database, messaging, and external API connections
  5. Stop background services: Cancellation tokens propagate shutdown signals

Environment Configuration

Environment Detection

ASP.NET Core uses ASPNETCORE_ENVIRONMENT to determine the environment:

Value Use Case Config File
Development Local development appsettings.Development.json
Docker Containerized environments appsettings.Docker.json
Production Production deployments appsettings.Production.json
Test Integration tests appsettings.Test.json

Configuration Priority

Configuration is merged in order (last wins):

  1. appsettings.json (base)
  2. appsettings.{Environment}.json (environment-specific)
  3. Environment variables
  4. Command-line arguments
  5. Azure App Configuration (if enabled)

Environment-Specific Behavior

if (environment.IsDevelopment())
{
    application.UseDeveloperExceptionPage();
}
else
{
    application.UseHsts();
}

#if Swagger && (UseGrpc || UseRestApi || UseAzureFunction)
// Swagger typically enabled only in Development
if (environment.IsDevelopment())
{
    application.UseMicroserviceSwagger();
}
#endif

Static Files and Default Files

Static Files Middleware

Serves files from wwwroot/ directory:

application.UseDefaultFiles();  // Serves index.html for root path
application.UseStaticFiles();   // Serves files from wwwroot/

SPA Fallback

For single-page applications:

application.UseDefaultFiles();
application.UseStaticFiles();

// Fallback to index.html for client-side routing
application.UseEndpoints(endpoints =>
{
    endpoints.MapFallbackToFile("index.html");
});

Static File Options

Customize static file serving:

application.UseStaticFiles(new StaticFileOptions
{
    OnPrepareResponse = ctx =>
    {
        // Add caching headers
        ctx.Context.Response.Headers.Append("Cache-Control", "public,max-age=3600");
    }
});

Security Considerations

HTTPS Enforcement

Always enforce HTTPS in production:

application.UseHttpsRedirection();

Configuration:

{
  "HttpsRedirection": {
    "RedirectStatusCode": 308,
    "HttpsPort": 5001
  }
}

HSTS (HTTP Strict Transport Security)

Enable HSTS for production:

if (!environment.IsDevelopment())
{
    application.UseHsts();
}

Reverse Proxy Security

  • Always validate forwarded headers via KnownProxies or KnownNetworks
  • Do not trust headers from untrusted sources
  • Use network-level IP restrictions for admin endpoints

Static File Protection

Protect sensitive static files:

application.UseStaticFiles(new StaticFileOptions
{
    OnPrepareResponse = ctx =>
    {
        if (!ctx.Context.User.Identity.IsAuthenticated)
        {
            ctx.Context.Response.StatusCode = 403;
            ctx.Context.Response.Body = Stream.Null;
        }
    }
});

Testing and Debugging

WebApplicationFactory

Integration tests use WebApplicationFactory:

public class MicroserviceTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> factory;

    public MicroserviceTests(WebApplicationFactory<Program> factory)
    {
        this.factory = factory;
    }

    [TestMethod]
    public async Task HealthCheck_ReturnsHealthy()
    {
        var client = this.factory.CreateClient();
        var response = await client.GetAsync("/health");

        Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
    }
}

Custom Host Configuration for Tests

Override configuration in tests:

var factory = new WebApplicationFactory<Program>()
    .WithWebHostBuilder(builder =>
    {
        builder.ConfigureAppConfiguration((context, config) =>
        {
            config.AddInMemoryCollection(new Dictionary<string, string>
            {
                ["Kestrel:Endpoints:Http:Url"] = "http://localhost:0"
            });
        });
    });

Logging in Tests

Capture logs during test execution:

var factory = new WebApplicationFactory<Program>()
    .WithWebHostBuilder(builder =>
    {
        builder.ConfigureLogging(logging =>
        {
            logging.ClearProviders();
            logging.AddConsole();
        });
    });

Best Practices

Do's

  1. Use Configuration-Driven Setup
  2. Avoid hardcoded ports, connection strings, or URLs
  3. Use appsettings.{Environment}.json for environment-specific values

  4. Enable Forwarded Headers

  5. Always enable when behind reverse proxy
  6. Configure known proxies for security

  7. Configure Thread Pool

  8. Set minimum threads to prevent starvation
  9. Scale based on expected workload

  10. Use Health Checks

  11. Expose /health, /alive, /startup endpoints
  12. Integrate with orchestrator probes

  13. Implement Graceful Shutdown

  14. Flush telemetry on shutdown
  15. Stop background services cleanly
  16. Close connections gracefully

  17. Separate Concerns

  18. Use Startup class for clear separation
  19. Extend via extension methods
  20. Keep Program.cs minimal

Don'ts

  1. Don't Block Startup

    // ❌ BAD - Blocks thread pool
    var result = someService.GetData().Result;
    
    // ✅ GOOD - Async initialization
    await someService.InitializeAsync();
    

  2. Don't Hardcode Ports

    // ❌ BAD
    webBuilder.UseUrls("http://localhost:5000");
    
    // ✅ GOOD
    // Use appsettings.json or ASPNETCORE_URLS
    

  3. Don't Trust All Forwarded Headers

    // ❌ BAD - Allows header spoofing
    options.ForwardedHeaders = ForwardedHeaders.All;
    
    // ✅ GOOD - Whitelist known proxies
    options.KnownProxies.Add(IPAddress.Parse("10.0.0.1"));
    

  4. Don't Skip HTTPS in Production

    // ❌ BAD - No HTTPS enforcement
    // Missing UseHttpsRedirection()
    
    // ✅ GOOD
    application.UseHttpsRedirection();
    

  5. Don't Expose Detailed Errors in Production

    // ❌ BAD
    if (environment.IsProduction())
    {
        application.UseDeveloperExceptionPage(); // Never!
    }
    
    // ✅ GOOD
    if (environment.IsDevelopment())
    {
        application.UseDeveloperExceptionPage();
    }
    else
    {
        application.UseHsts();
    }
    

Troubleshooting

Common Issues

Issue Symptom Solution
Port Already in Use Service fails to start Check netstat or lsof, change port in config
gRPC Not Working UNAVAILABLE errors Ensure HTTPS and HTTP/2 are enabled
Health Checks Fail 404 on /health Verify MapMicroserviceHealthChecks() is called
Redirect Loops Infinite redirects Check forwarded headers configuration
Static Files Not Serving 404 on static assets Verify UseStaticFiles() is before UseRouting()
Slow Startup Long initialization time Profile startup, consider lazy loading
Thread Starvation Slow responses under load Increase thread pool minimum threads

Debugging Tips

  1. Enable Detailed Errors:

    {
      "Logging": {
        "LogLevel": {
          "Default": "Debug"
        }
      }
    }
    

  2. Check Middleware Order:

  3. Use app.Use(async (context, next) => { ... await next(); }) to trace execution

  4. Validate Configuration:

    var kestrelOptions = configuration.GetSection("Kestrel");
    // Log or validate configuration before binding
    

  5. Monitor Startup:

  6. Add startup logging to track initialization phases
  7. Use application insights or structured logging

Summary

Hosting in the ConnectSoft ConnectSoft Templates provides:

  • Unified Bootstrap: Consistent Program.cs and Startup pattern across all services
  • Kestrel Configuration: Declarative server setup via appsettings.json
  • Middleware Pipeline: Composable, ordered request processing pipeline
  • Multi-Platform Support: Docker, IIS, Linux systemd, Azure App Service
  • Reverse Proxy Support: Forwarded headers with security validation
  • Environment Awareness: Environment-specific configuration and behavior
  • Lifecycle Management: Startup warmup gates and graceful shutdown hooks
  • Security: HTTPS enforcement, HSTS, static file protection
  • Observability: Integrated logging, metrics, and tracing from startup

By following these patterns, applications achieve:

  • Consistency — All services follow the same hosting patterns
  • Flexibility — Supports multiple deployment targets
  • Reliability — Graceful startup and shutdown
  • Security — Secure-by-default configuration
  • Observability — Full lifecycle monitoring and logging
  • Maintainability — Clear separation of concerns and testability

The hosting infrastructure ensures that ConnectSoft applications are production-ready, secure, and maintainable across any deployment environment.