Skip to content

This plan outlines the migration of your PowerShell-based deployment process (see Deployment.psm1) remote-execution model (PowerShell WinRM / C# WMI) to a high-performance, asynchronous Client-Server Architecture using .NET 10, Minimal APIs, and .NET Channels.


Deployment Automation: Architectural Refactoring Plan#

1. Executive Summary#

The goal is to replace the current PowerShell remote-execution model with a specialized Deployment Agent (Server API) and a Deployment Orchestrator (Client).

By using .NET Channels, the Server API handles heavy operations (file extraction, IIS configuration) in the background, preventing HTTP timeouts. By moving SQL Logic to the Client, we ensure that database registration only happens if the local server operations succeed, providing a "Two-Phase Commit" feel to the deployment.

2. Architecture Diagram#

graph TD
    subgraph Client [Deployment Orchestrator - Admin PC / CI/CD]
        CO[Orchestrator Logic]
        Dapper[(Dapper / SQL Client)]
    end

    subgraph Server [Deployment Agent - Target VM]
        direction TB
        API[Minimal API Endpoint]
        Chan[[.NET Channel - In-Memory Queue]]
        BGS[Background Worker Service]

        subgraph LocalTasks [Local System Operations]
            IIS[Microsoft.Web.Administration]
            SVC[System.ServiceProcess]
            FS[System.IO.Compression]
        end
    end

    subgraph DB [Database Server]
        SQL[(CommandCenter DB)]
    end

    %% Flow
    CO -->|1. Pre-Check| SQL
    CO -->|2. POST Zip + JSON| API
    API -->|3. Queue Job| Chan
    Chan -->|4. Process| BGS
    BGS -->|5. Manage| LocalTasks
    CO -->|6. Poll Status| API
    CO -->|7. Finalize/Activate| SQL
Hold "Alt" / "Option" to enable pan & zoom

3. Technology Stack & Requirements#

Component Technology Purpose
Runtime .NET 10 (Windows TFM) Core framework (requires net10.0-windows)
API ASP.NET Core Minimal APIs Lightweight entry point for deployment requests
Backgrounding System.Threading.Channels Thread-safe, in-memory producer/consumer queue
IIS Management Microsoft.Web.Administration Local IIS AppPool, Site, and WebApp management
Database Dapper & Microsoft.Data.SqlClient Fast SQL execution for CommandCenter registration
Service Mgmt System.ServiceProcess.ServiceController Controlling and installing Windows Services

4. Server-Side Implementation (The Agent)#

The Server API acts as a "Local Agent." It receives the deployment package and immediately returns a 202 Accepted status while processing the task in the background.

A. The Deployment Task & Channel#

The API uses a Channel<T> to pass data between the HTTP thread and the Background Service.

public record DeploymentJob(
    Guid JobId, 
    DeploymentPayload Config, 
    string TempPackagePath);

public record DeploymentPayload(
    string Environment, 
    string Version, 
    string Package,
    string FormattedAppVersion);

B. Minimal API Implementation#

The API receives the multipart/form-data request, saves the zip to a temporary location, and queues a job.

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton(Channel.CreateUnbounded<DeploymentJob>());
builder.Services.AddSingleton<JobStatusStore>(); // Custom Dictionary wrapper for status tracking
builder.Services.AddHostedService<DeploymentWorker>();

var app = builder.Build();

app.MapPost("/deploy", async (IFormFile file, [FromForm] string json, Channel<DeploymentJob> channel, JobStatusStore status) => {
    var config = JsonSerializer.Deserialize<DeploymentPayload>(json);
    var jobId = Guid.NewGuid();

    // 1. Save file locally
    var tempPath = Path.Combine(Path.GetTempPath(), $"{jobId}.zip");
    using (var fs = new FileStream(tempPath, FileMode.Create)) await file.CopyToAsync(fs);

    // 2. Queue Job
    await channel.Writer.WriteAsync(new DeploymentJob(jobId, config, tempPath));
    status.Update(jobId, "Queued");

    return Results.Accepted($"/status/{jobId}", new { JobId = jobId });
});

app.MapGet("/status/{id}", (Guid id, JobStatusStore status) => Results.Ok(status.Get(id)));

C. Background Worker (The Heavy Lifter)#

This service consumes the channel and performs the local PowerShell-equivalent tasks.

public class DeploymentWorker : BackgroundService {
    protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
        await foreach (var job in _channel.Reader.ReadAllAsync(stoppingToken)) {
            try {
                // Logic based on your PowerShell 'Install-BenefitManager'
                // 1. Generate TargetPath locally (e.g. D:\Deployments\Productie\25.5.29)
                // 2. ZipFile.ExtractToDirectory(job.TempPackagePath, targetPath);
                // 3. ConfigureIIS(job.Config, targetPath);
                // 4. InstallWindowsService(job.Config, targetPath);

                _statusStore.Update(job.JobId, "Completed");
            } catch (Exception ex) {
                _statusStore.Update(job.JobId, $"Failed: {ex.Message}");
            }
        }
    }
}

5. Client-Side Implementation (The Orchestrator)#

The client performs the high-level orchestration, ensuring the database is updated only when the server is ready.

A. Phase 1: Pre-Check (SQL)#

Using Dapper, check if the version is already active or if the environment is ready.

using var conn = new SqlConnection(connectionString);
var isDeployed = await conn.ExecuteScalarAsync<bool>(
    "SELECT COUNT(1) FROM TblApplicationVersion WHERE Identifier = @Version", 
    new { config.Version });

B. Phase 2: Trigger Agent (HTTP)#

Send the ZIP file and the JSON configuration to the Server API.

C. Phase 3: Poll for Completion#

The client loops (with a timeout) calling the /status/{id} endpoint until it receives "Completed".

D. Phase 4: Finalize Deployment (SQL)#

Only after the Server API confirms the files are extracted and IIS is configured, the client runs the MERGE SQL logic to set Online = 1 and Active = 1.


6. Implementation Checklist#

  1. Preparation:
    • Define a standard base path on all servers (e.g., D:\BenefitManager\Deployments).
    • Create a "Local Agent" Windows Service project for the Server API.
  2. Server Agent Development:
    • Implement Microsoft.Web.Administration logic to replace New-IISApplication.
    • Implement System.ServiceProcess logic to replace Install-BenefitManagerService.
    • Secure the API with an API Key or Windows Authentication.
  3. Client Development:
    • Port the PowerShell SQL scripts into C# string constants or embedded resources.
    • Implement the polling logic with System.Net.Http.
  4. Testing:
    • Verify that Uninstall correctly stops services and removes IIS Applications without deleting shared AppPools.
    • Test "Rollback" by having the client point the SQL VirtualDirectory back to the previous version if the Agent fails.

7. Data Sample: Full Payload#

The client sends this JSON to the API inside the multipart/form-data request:

{
  "Environment": "Productie",
  "Build_DefinitionName": "Azure Deployment",
  "Package": "BenefitManager_25.5.29.zip",
  "Version": "25.5.29",
  "Id": "48270",
  "FormattedAppVersion": "Productie_25.5"
}
Note: The Server API uses the Environment and Version to calculate the local TargetPath automatically, ensuring consistency across the fleet.

8. Summary of Benefits#

  1. Security: No need to open WinRM/WMI ports across environments. The API handles its own local security context.
  2. Reliability: Uploading a zip file over HTTP is much faster and less prone to network drops than copying files over UNC paths (\\server\c$\...).
  3. Maintainability: The C# code is strongly typed. The API can easily be extended with Swagger/OpenAPI to test deployments manually.
  4. Clean Uninstall: The API inherently knows where things are installed locally, making the Uninstall process (stopping services, removing IIS bindings, deleting directories) safe and contained.