A single CLI to start, stop, restart, health-check, and bust the cache of web apps regardless of how they're hosted on Windows — IIS sites/pools, self-hosted Kestrel processes, Windows services, or Docker containers.
.NET 8 · Windows-only · zero external services · configured from a single
appsettings.json
On a typical Windows host you end up juggling iisreset, sc.exe, Get-Process, docker stop, and a pile of admin endpoints just to bounce one app and clear its cache. wwsm reads one config file, figures out the hosting model per app, and exposes a uniform verb set on top.
flowchart LR
User([User]) -->|wwsm start orders-api| CLI[wwsm CLI]
CLI --> Dispatcher{Verb<br/>Dispatcher}
Dispatcher --> Repo[(WebAppRepository<br/>appsettings + IIS discovery)]
Dispatcher --> Factory[WebAppManager<br/>Factory]
Factory -->|Iis| IIS[IIS<br/>Manager]
Factory -->|Kestrel| Kestrel[Kestrel<br/>Process Mgr]
Factory -->|WindowsService| WinSvc[Windows Service<br/>Mgr]
Factory -->|Docker| Docker[Docker<br/>Mgr]
IIS --> IISHost[(IIS<br/>ServerManager)]
Kestrel --> Proc[(OS Process)]
WinSvc --> SCM[(Service<br/>Control Mgr)]
Docker --> Dockerd[(docker CLI)]
IIS & Kestrel & WinSvc & Docker --> Health[HealthCheck<br/>Service]
Health --> HTTP[(HTTP<br/>health URL)]
# Build
dotnet build
# Run from the build output
.\bin\Debug\net8.0-windows\wwsm.exe list
# Or via dotnet run
dotnet run -- status orders-api
dotnet run -- restart billing-svc
dotnet run -- clear-cache orders-api --method http| Verb | Aliases | Purpose |
|---|---|---|
list |
ls |
Show all managed apps with status + health |
status <id> |
Detailed status for one app | |
start <id> |
Start an app | |
stop <id> |
Stop an app | |
restart <id> |
Stop then start | |
clear-cache <id> |
clearcache, reset-cache |
Run a cache reset strategy. --method recycle|http|purge-files; omit to be prompted. |
help |
--help, -h, /? |
Print usage |
Two strategy-pattern families do all the real work. Both register multiple implementations of the same interface in DI and resolve to one by an enum key — adding a new hosting model or cache method is "implement the interface and register it" in ServiceCollectionExtensions.
classDiagram
class IWebAppManager {
<<interface>>
+WebAppType SupportedType
+GetStateAsync(app, ct) Result~WebAppState~
+StartAsync(app, ct) Result
+StopAsync(app, ct) Result
+RestartAsync(app, ct) Result
}
class WebAppManagerFactory {
-Dictionary~WebAppType,IWebAppManager~ _byType
+Resolve(WebAppType) IWebAppManager
}
class IisWebAppManager
class KestrelProcessManager
class WindowsServiceWebAppManager
class DockerWebAppManager
IWebAppManager <|.. IisWebAppManager
IWebAppManager <|.. KestrelProcessManager
IWebAppManager <|.. WindowsServiceWebAppManager
IWebAppManager <|.. DockerWebAppManager
WebAppManagerFactory o-- IWebAppManager : has many
classDiagram
class ICacheResetStrategy {
<<interface>>
+CacheResetMethod Method
+IsApplicable(app) bool
+ExecuteAsync(app, ct) Result
}
class CacheResetCoordinator {
+AvailableFor(app) CacheResetMethod[]
+ExecuteAsync(app, method, ct) Result
}
class RecycleCacheReset {
Restarts host;
recycles IIS pool in-place
}
class HttpCacheReset {
POSTs configured admin endpoint
}
class FileCacheReset {
Purges configured directory
}
ICacheResetStrategy <|.. RecycleCacheReset
ICacheResetStrategy <|.. HttpCacheReset
ICacheResetStrategy <|.. FileCacheReset
CacheResetCoordinator o-- ICacheResetStrategy : has many
Recycle is always applicable (it falls back to RestartAsync); HttpEndpoint requires Cache.HttpEndpoint.Url; PurgeFiles requires Cache.PurgePath.
sequenceDiagram
autonumber
actor U as User
participant D as CommandDispatcher
participant R as WebAppRepository
participant F as WebAppManagerFactory
participant M as IWebAppManager (resolved)
participant H as HealthCheckService
U->>D: wwsm restart orders-api
D->>R: FindAsync("orders-api")
R-->>D: WebAppDescriptor (Type=Kestrel)
D->>F: Resolve(Kestrel)
F-->>D: KestrelProcessManager
D->>M: StopAsync(app)
M-->>D: Result.Ok
D->>M: StartAsync(app)
M-->>D: Result.Ok
D->>M: GetStateAsync(app)
M->>H: CheckAsync(HealthUrl)
H-->>M: 200 OK
M-->>D: WebAppState(Running)
D-->>U: ✓ printed via ConsoleRenderer
WebAppStatus folds OS-level state and HTTP health into one value the CLI renders. The interesting transition is Running + bad health → Unhealthy — a process that's up but failing health is not reported as Running.
stateDiagram-v2
[*] --> Unknown
Unknown --> Stopped
Stopped --> Starting : start
Starting --> Running : process up
Running --> Unhealthy : health URL fails
Unhealthy --> Running : health URL recovers
Running --> Stopping : stop
Unhealthy --> Stopping : stop
Stopping --> Stopped
Stopped --> NotFound : descriptor gone
Running --> NotFound : descriptor gone
Apps are read from appsettings.json under WebServiceManager:Applications, merged with IIS auto-discovery (toggle via WebServiceManager:IisAutoDiscovery). Static config wins on Id collisions; discovered IIS apps get synthetic Ids like iis-site:Default Web Site / iis-pool:DefaultAppPool.
flowchart LR
A[appsettings.json] -->|bind| Opts[WebServiceManagerOptions]
Opts --> Apps[Static<br/>Applications]
IIS[IIS ServerManager] -->|sites + pools| Disc[IisDiscoveryService]
Apps --> Merge{{Merge by Id<br/>static wins}}
Disc --> Merge
Merge --> Cache[(Cached for<br/>process lifetime)]
Cache --> CLI[CLI commands]
- Windows 10/11 or Windows Server with .NET 8 SDK
Microsoft.Web.Administration(bundled via NuGet) — IIS must be installed if you use theIistype or auto-discoverydockeronPATHif you use theDockertype- Sufficient privileges for the operation: stopping/starting Windows services and IIS sites usually requires an elevated shell
Two GitHub Actions workflows under .github/workflows/:
- ci.yml — runs on every push to
mainand PR →main. Restores → builds the solution (Release) → runs the xunit suite (99 tests) → uploads.trxas artifact. Pinned towindows-latestbecause the manager talks to IIS / Windows Services APIs. - release.yml — runs when a
v*tag is pushed. Publishes two flavors of the CLI and attaches them to an auto-created GitHub Release with auto-generated notes from the commit history:wwsm-<ver>-win-x64-selfcontained.zip— single-filewwsm.exewith the .NET 8 runtime bundled (~70 MB, no install needed)wwsm-<ver>-win-x64-framework-dependent.zip— smaller (~5 MB) but requires .NET 8 Runtime on the target machine
To cut a release:
git tag v1.0.0
git push origin v1.0.0Dependabot (.github/dependabot.yml) opens weekly PRs every Monday for outdated NuGet packages (main and Tests projects, separately) and for the GitHub Actions versions themselves. Labels: deps / deps tests / ci, capped at 5/3/unlimited PRs respectively.
{ "WebServiceManager": { "IisAutoDiscovery": true, "HealthCheck": { "TimeoutSeconds": 5 }, "Operation": { "StateTransitionTimeoutSeconds": 30, "MaxRetries": 3 }, "Applications": [ { "Id": "orders-api", "Type": "Kestrel", "HealthUrl": "http://localhost:5010/health", "Process": { "ExecutablePath": "C:\\apps\\orders\\Orders.Api.exe" }, "Cache": { "HttpEndpoint": { "Url": "http://localhost:5010/admin/cache/clear", "Method": "POST" }, "PurgePath": "C:\\apps\\orders\\cache" } }, { "Id": "billing-svc", "Type": "WindowsService", "ServiceName": "BillingWebApi" }, { "Id": "frontend-docker", "Type": "Docker", "ContainerName": "frontend-web" } ] } }