Program and Hosting
Program.cs is the entire host of the HiNC web service: one class whose Main builds an ASP.NET
Core application and runs it. It presents no screen of its own — it is the process that starts and
stops HiAPI, registers every service the controllers and hubs resolve, maps those controllers and
the eight SignalR hubs, serves the built Quasar SPA out of the web root, and falls back to the SPA
shell for every URL the Vue router owns. BuildApp is public and returns the built application
without starting it, so the build phase and the run phase are separable; Main calls it, writes one
startup line to the log, and runs.
HiAPI Lifetime
The host brackets its whole life in one pair of HiAPI calls.
AppBegin(API) runs once during BuildApp, after the
service container is built and before the request pipeline is configured. It logs in every licence
License holds, initialises the display engine (DispEngine), and
opens the SQLite step-cache database at the path it is given, publishing it as
SqliteStepStorage's default instance. The overload the webservice calls takes a
logger and a cache-database path.
AppEnd(API) unwinds the same set in reverse: it waits for queued background CubeTree frees to drain so no native delete runs against a half-torn-down runtime, disposes the step and identity storages, shuts the display engine down, and logs the licences out.
Three separate events can reach AppEnd, and a static latch guarantees it runs at most once:
IHostApplicationLifetime.ApplicationStopping— the ordinary graceful stop.AppDomain.CurrentDomain.ProcessExit— which still fires when a console window is closed outright.Console.CancelKeyPress— Ctrl+C and Ctrl+Break, which cancel the immediate termination and ask the lifetime for a graceful stop instead, so the first path runs.
The cache-database path is Cache/{CacheDbId}.db beneath the admin directory.
CacheDbId is the HiNC:CacheDbId configuration value when one is set and
the hosting environment name otherwise. Supplying a path matters:
SqliteStepStorage given none falls back to a single per-user file, so several
instances on one account would otherwise share it. Two instances launched under different
--environment names get different cache files with no further configuration.
Startup Order
Three registrations run before the builder exists, because each seeds a table that is read — or frozen — the moment anything else touches it:
LocalProjectService.Reg()fills XFactory's default generator table with every type the simulation pipeline may deserialise, which is what every project XML read resolves against.UserConfig.Reg()adds the per-user preference file's own type to the same table; without it, a persisted preference file cannot be read back and everyUserServiceresolution fails once one exists.- Lang is appended to StringLocalizer's extended type list, so session-command titles resolve through that assembly's satellite resources. The type list is static, but each localizer builds its own resource-manager list lazily on its first lookup and keeps it, so a type added after a given localizer has been used never reaches that localizer. Registering before the builder exists is what puts the addition ahead of every first lookup.
After the container is built and before the pipeline is assembled, the host wires the native core's
log output into the application logger through CppLogUtil, sets the cache
identifier, calls AppBegin, and then runs
Seed(API) over the admin Resource root. The
seeder copies in only the marked .default items, leaves unmarked items alone as user property, and
skips the whole pass when its version stamp already matches the shipped resource assembly.
Registered Services
Every application service is registered as a singleton. The repository contains no AddScoped
and no AddTransient call, so no service this host registers has a per-request lifetime and any
state one of them holds is process-wide. Any shorter lifetime in the container is one of the
framework's own registrations rather than an application service.
Four registrations are worth reading closely:
UserServiceis built by a factory rather than by type, so that its configuration path is assigned unconditionally — including when no preference file exists yet, which is the only way the first save can create one. When the file does exist, the factory deserialises it through XFactory.ProxyConfigis bound by hand into a plain singleton from theProxyConfigconfiguration section. A separateservices.Configure<ProxyConfig>call also registers it through the options system, but nothing resolvesIOptions<ProxyConfig>; the plain singleton is what ProxyProjectService and the startup code receive. That binding happens once at startup and is not reloaded.- ProxyProjectService is registered twice — once under its own type and once as IProjectService — so the container builds one instance per registration. Both hold nothing but the same two injected singletons, LocalProjectService and ProxyConfig, which is where all the state lives.
AuthConfigis bound from theAuthsection and registered as an instance, so the authentication controller and the pipeline decision below read the same object. See Login and Authentication for what it switches on.
Six singletons are resolved eagerly at the end of BuildApp so they subscribe to their engine events
at startup rather than when the first client happens to connect: the CL-strip broadcast service, the
four per-sink message broadcast services, and the NC-program registry.
Kestrel is configured with AllowSynchronousIO = true, which permits handlers to write to the
response body synchronously.
Controllers
Controllers are added with AddControllers(), with the host's own assembly registered as an explicit
application part, and reached by MapControllers() at the end of the pipeline. Every controller in
the host assembly is attribute-routed, and every route template begins with api/ — most as
api/[controller], the rest as an explicit kebab-case path such as api/mech/machine-tool or
api/execution/cl-strip. No controller route sits outside api/, which is what makes the
“everything else is the SPA” fallback rule below safe.
One controller is exempt from the login gate: the authentication controller carries
[AllowAnonymous] so it stays reachable when the fallback policy locks everything else.
SignalR Hubs
AddSignalR() is called without protocol configuration, and eight hub routes are mapped:
| Route | Hub type | What it carries |
|---|---|---|
/renderingHub |
RenderingHub |
server-rendered canvas frames and the input that drives them, one display engine per connection |
/shellMessageHub |
ShellMessageHub |
session-level routine and lifecycle messages |
/ncDiagnosticHub |
NcDiagnosticHub |
NC-pipeline diagnostics |
/stepDiagnosticHub |
StepDiagnosticHub |
step-anchored diagnostics |
/ncManipulationDiagnosticHub |
NcManipulationDiagnosticHub |
NC-manipulation diagnostics — writeback conversion and optimisation |
/executionStatusHub |
ExecutionStatusHub |
ExecutionStatusUpdated, SessionCursor and SessionStatusMessage, broadcast to all clients; the client-callable GetExecutionStatus() answers the caller alone, on the same ExecutionStatusUpdated name |
/clStripHub |
ClStripHub |
strip-chart display range, selected and entered step, and throttled data-update hints |
/cleanupHub |
CleanupHub |
index keys, and optional follow-up action keys, that a client registers against its connection with Add; the disconnect sweep finds none of them — see below |
The four message hubs share one base class and one contract — the client calls GetMessages(limit)
and the server answers on MessagesUpdate — but they are deliberately separate routes rather than
one hub with a discriminator, so a client subscribes to exactly the sink it wants and an idle panel
means an idle hub rather than a lost connection.
The cleanup hub is the one whose registry does not outlive the call that fills it. SignalR builds a
fresh hub instance for every invocation, and the key dictionary Add writes to is an instance
member rather than shared state, so the dictionary the disconnect handler walks is a different,
empty one and releases nothing. Only IndexService — a singleton — is shared across those
instances. The client does not depend on the hub for the release: useCleanupHub.ts keeps its own
set of registered keys and posts each one to /api/Index/Remove before the component unmounts. That
is the path that actually frees an entry.
JSON Serialisation
The controllers' serializer options carry two settings, both applied through AddJsonOptions.
JsonNumberHandling.AllowNamedFloatingPointLiterals. Several engine values use
double.PositiveInfinity as their “no limit” state — the optimizer's Max Feed Per Tooth and
Preferred Force among them — and the snapshots the SPA reads carry those doubles straight into the
response object. System.Text.Json refuses to write an infinite or NaN double as a JSON number, so
without this setting every such response would fault during serialisation instead of returning. With
it, those values travel as the JSON strings "Infinity", "-Infinity" and "NaN". The reading side
is not uniform: the numeric input widgets take all three spellings case-insensitively, while the
option-snapshot reader in the mission API recognises the two infinity spellings only and resolves
anything else non-numeric — "NaN" included — to the fallback its caller passed, which is positive
infinity for Max Feed Per Tooth and Preferred Force.
Numeric Input/Output covers the client half of that boundary, and
NC Optimization Option Panel (NC Optimization Config) the panel that leans hardest on it — including the two write
endpoints that take a string body so the same spelling survives the round trip.
JsonStringEnumConverter. Enums are read and written as their names rather than as integers,
which is what the API wrappers expect and what keeps the OpenAPI schema self-describing.
Both settings belong to the MVC controllers only. Hub payloads are serialised by SignalR's own protocol, which is left at its defaults, so neither the named floating-point literals nor the string enums extend to hub messages.
The Request Pipeline
The order the middleware is added in is load-bearing, and the host sets it explicitly rather than accepting the default arrangement:
- Forwarded headers first, honouring
X-Forwarded-ForandX-Forwarded-Protoso that every later stage sees the real client scheme. Both the known-proxy and the known-network lists are cleared, so those headers are accepted from any caller. - Swagger — the Swashbuckle document and its UI, both middleware.
MapOpenApibeside them registers the framework's own document as an endpoint instead, so that one is reached at step 8 with everything else that is routed. - Default files, then static files, serving the built SPA and everything else physically present in the web root.
UseRouting()— placed here on purpose. An application that never calls it gets routing inserted at the front of the pipeline instead, which would select the SPA fallback endpoint before the static-file middleware ran; that middleware stands down once an endpoint is chosen, so a directory URL under the web root would be answered by the Vue router's not-found view rather than by the directory's own default document.- CORS, applying the
AllowAllpolicy described below. - HTTPS redirection, skipped in the Development environment.
- Authentication, then authorization. Both are no-ops when the login gate is off, because no scheme and no fallback policy are registered in that case.
- Endpoints — controllers, the eight hubs, and the two SPA fallbacks.
A request answered before step 7 never reaches the authorization stage at all: the static-file
middleware and the Swagger middleware both sit ahead of it and short-circuit the request when they
match. The two SPA fallbacks are endpoints rather than middleware, so they do reach it, and both are
marked AllowAnonymous — which is what lets the SPA shell, carrying no data of its own, load and
show its own login view when the gate is on.
The SPA Fallback
Two fallback registrations serve index.html:
util/file-explorer/{**location}— an explicit pattern with no file-name constraint.- The bare fallback, for everything else.
The bare form's implicit route pattern carries a nonfile constraint that rejects any URL whose last
segment contains a dot. File Explorer deep links mirror real folder and file names into the URL, and
those names routinely contain dots, so reloading or pasting such a location needs the explicit
pattern. Genuine static assets cannot be shadowed by it, because the static-file middleware has
already run by the time either fallback is reached.
Serving the SPA
The Quasar build writes its output directly into the host's web root — the front end's build
configuration sets its distribution directory to the web service's wwwroot — so there is no copy
step between building the SPA and serving it. The router runs in history mode, which is what makes
the SPA fallback necessary: every client-side route is an extension-less URL the server has no
endpoint for.
The front-end build script runs the i18n census and glossary lint before invoking the Quasar build, so a build that fails those checks produces no new bundle.
For front-end work the Quasar dev server runs on its own port and proxies the routes the backend owns
to the running service. That proxy list holds /api, /swagger, /renderingHub,
/executionStatusHub, /clStripHub, /cleanupHub, and /sessionMessageHub — a path the host maps
no hub on. The four per-sink message hubs are absent from it, so the Session Messages panels receive
no live messages through the dev server.
OpenAPI and Swagger
A single private constant holds the REST contract version. It is simultaneously the Swashbuckle document group name — so it appears in the document's route — and the version the UI displays, which is why it is defined in one place. It is raised by hand on a breaking REST change only, and is deliberately not tied to the assembly version or to the project-file format version.
Both OpenAPI surfaces are exposed in every environment rather than behind a development gate, so an
automated caller always has a machine-readable contract. The Swashbuckle UI is served at /swagger
and its document at /swagger/{version}/swagger.json; the framework's own document is published
under /openapi/.
Schema ids are keyed on the full type name with the nested-type separator normalised. Without that,
two controllers' nested request types that share a simple name collide and the document generator
throws — which surfaces as an HTTP 500 from the document route rather than as a startup failure. XML
documentation comments are folded in from the host assembly and from every referenced assembly whose
name starts with Hi. Every read is skipped when its file is missing, and each referenced-assembly
read carries a catch-all of its own on top of that, so one whose file is present but unreadable is
ignored; the host assembly's read has no such catch-all.
CORS
One policy, named AllowAll, permits any origin, any method and any header, and it is applied
globally. It does not allow credentials, so the response carries a wildcard origin and a cross-origin
browser call cannot use the session cookie. The SPA is served from the same origin as the API and the
hubs, so this constrains only callers hosted elsewhere.
Logging
The default providers are cleared and two are added: a console provider, and a daily file provider
rooted at a logs folder beneath the process's current working directory. The file provider writes
one line per entry to log-{yyyy-MM-dd}.txt and swallows every I/O failure, on the principle that
logging must not fault a request or a startup.
The two providers are filtered independently. The console follows the Logging:LogLevel section of
configuration, which sets everything to Warning. The file provider carries its own code-set rules —
Information for the application's own categories, Warning for Microsoft and System — and
provider-specific rules take precedence over configuration's provider-neutral ones, so the file stays
useful while the console stays quiet.
Main writes one Information line immediately after the application is built, so the file for the day
the service starts has content before the first request arrives. That guarantee stops at midnight: the
provider recomposes the file name from the clock on every append, and creates nothing ahead of time, so
a host left running into a new day has no file for it until the next entry is written.
GET /api/project/logs reads the same directory.
Configuration
Configuration is the standard chain: appsettings.json, then appsettings.{Environment}.json, then
environment variables, then command-line arguments. The keys this host reads are:
| Key | What it controls |
|---|---|
Logging:LogLevel |
the console provider's levels |
AllowedHosts |
host filtering; shipped as a wildcard |
Kestrel:Endpoints |
the addresses the server binds |
ProxyConfig:AdminDirectory |
the admin working root, under which Resource and Cache live |
Auth |
the optional login gate |
HiNC:CacheDbId |
an explicit step-cache database id, overriding the environment name |
The Development overlay repeats the logging, admin-directory and authentication sections and declares
no Kestrel section, so the listening address is the same under either environment.
Kestrel Endpoints Win
Kestrel:Endpoints is the authoritative source of the listening address. When it declares endpoints,
Kestrel binds those and ignores addresses supplied any other way — ASPNETCORE_URLS, --urls,
and the launch profile's applicationUrl alike — logging that it is overriding them. The shipped
configuration declares one HTTP endpoint on loopback port 5000. The http launch profile names the
same address, so the override is invisible there; the https profile also names an HTTPS address on
a second port, and that address is one of the ones Kestrel discards, so launching under it still
yields plain HTTP on 5000 and nothing else.
To move a local instance to another port, set the Kestrel__Endpoints__Http__Url environment
variable, which the environment-variable provider maps onto the same key. Setting ASPNETCORE_URLS
alone changes nothing.
The endpoint binds localhost, so the shipped configuration answers only on the machine it runs on.
Reaching it from elsewhere means either overriding that key or placing a reverse proxy in front,
which is the arrangement the forwarded-headers configuration handles.
Environment and Working Directory
The environment name reaches further here than usual. Both launch profiles set it to Development,
which selects the Development overlay, skips HTTPS redirection, and — unless HiNC:CacheDbId
overrides it — names the step-cache database file.
Two paths follow the process's current working directory rather than the content root: the logs
folder the file logger writes and the log endpoint reads, and the per-user preference file. An
instance launched from the project directory finds both where they are expected; one launched from
elsewhere puts them beside wherever it was started.
Source Code Path
See HiNC App Anatomy for git repository links.
Web Application
HiNC-2025-webservice (Quasar CLI SPA):
Program.cs— the whole subject of this page: the pre-builder registrations, every service registration, the JSON and Swagger options, the CORS policy, the pipeline order, the eightMapHubcalls, the two SPA fallbacks, theAppBegin/AppEndpair and the three shutdown triggers.appsettings.json— the shipped listening address, the admin directory and the login section.appsettings.Development.json— the Development overlay, which declares noKestrelsection.Properties/launchSettings.json— the two launch profiles whoseapplicationUrltheKestrelsection overrides.Common/DailyFileLoggerProvider.cs— the per-day file logger the log endpoint reads back.Common/AuthConfig.cs— the shape bound from theAuthsection.Common/AuthController.cs— the anonymous carve-out that keeps login reachable under the fallback policy.Common/NamedRootResolver.cs— resolves the admin, project and resource roots the seeder and the file endpoints work against.Common/CleanupHub.cs— the cleanup hub, and the per-invocation dictionary its disconnect handler walks.Common/IndexController.cs— the index remove endpoint the client calls before unmount.Disp/RenderingHub.cs— the canvas hub.Execution/SessionSinkHub.cs— the shared base and the four per-sink message hubs.Execution/ExecutionStatusHub.cs— the status, cursor and session-message hub.Execution/ClStripHub.cs— the strip-chart hub.Environments/UserConfig.cs— the preference type registered before the builder is created.Environments/ProjectController.cs— the log endpoints that read the daily file.wwwroot-src/src/composables/useCleanupHub.ts— the client's own key set and the index-remove calls that release it.wwwroot-src/src/api/mission.ts— the option-snapshot reader that maps the named-literal strings back to numbers, and the string-bodied setters that send them.wwwroot-src/quasar.config.ts— the build output directory that puts the bundle in the web root, the history router mode, and the dev-server proxy list.wwwroot-src/package.json— the build script that runs the i18n lint before the Quasar build.
HiAPI Engine
HiNc/HiNcKits/LocalApp.cs—AppBeginandAppEnd: licence log-in and log-out, display-engine start and finish, and the step-storage open and dispose.HiNc/HiNcKits/HiNcHost.cs— the cache-database identifier the host assigns at startup.HiNc/HiNcKits/ProxyConfig.cs— the admin-directory setting and its own default.HiNc/HiNcKits/ResourceSeeder.cs— the marked-defaults seeding pass and its version stamp.HiNc/SqliteUtils/SqliteStepStorage.cs— the step cache, and the per-user default path that makes an explicit per-instance path necessary.HiNc/MachiningProcs/ProxyProjectService.cs— the project service registered under two service types, holding only the singletons it is given.HiGeom/Common/StringLocalizer.cs— the static extended type list, and the per-instance resource-manager list each localizer builds on its own first lookup.
See Also
- Session State — what survives a project change inside the singletons this host registers, and what is rebuilt
- Login and Authentication — the
Authsection this host binds, and the cookie scheme and fallback policy it registers when the gate is on - Rendering Canvas on Web Service Application — the canvas hub this host maps, and the per-connection display engine behind it