ForgeTrust.AppSurface.Core
0.1.0-preview.4
dotnet add package ForgeTrust.AppSurface.Core --version 0.1.0-preview.4
NuGet\Install-Package ForgeTrust.AppSurface.Core -Version 0.1.0-preview.4
<PackageReference Include="ForgeTrust.AppSurface.Core" Version="0.1.0-preview.4" />
<PackageVersion Include="ForgeTrust.AppSurface.Core" Version="0.1.0-preview.4" />
<PackageReference Include="ForgeTrust.AppSurface.Core" />
paket add ForgeTrust.AppSurface.Core --version 0.1.0-preview.4
#r "nuget: ForgeTrust.AppSurface.Core, 0.1.0-preview.4"
#:package ForgeTrust.AppSurface.Core@0.1.0-preview.4
#addin nuget:?package=ForgeTrust.AppSurface.Core&version=0.1.0-preview.4&prerelease
#tool nuget:?package=ForgeTrust.AppSurface.Core&version=0.1.0-preview.4&prerelease
ForgeTrust.AppSurface.Core
The foundation of the AppSurface ecosystem. This package defines the core abstractions, the startup pipeline, and the module system that powers all other AppSurface libraries.
Overview
The Core library is designed to be lightweight and implementation-agnostic. It provides the infrastructure to:
- Define Modules (
IAppSurfaceModule,IAppSurfaceHostModule) that encapsulate logic. - Manage Dependency Graphs between modules.
- Provide a consistent Startup Pipeline (
AppSurfaceStartup) that sits on top of the .NET Generic Host.
Release Guidance
AppSurface is preparing the first coordinated v0.1.0 release. Before installing this package from a prerelease feed, read the v0.1 release preview for current release risk, provisional migration guidance, and the finalization path to the tagged release note.
Key Concepts
IAppSurfaceModule: The base interface for any unit of functionality that needs to register services or configure the application.StartupContext: Provides metadata about the running application, including the user-facing application label, assembly-backed host identity, application discovery assembly, environment, and startup-level console output mode.ConsoleOutputMode: Shared core enum that lets console-oriented packages describe whether command output should remain host-centric or command-first.AppSurfaceStartup: The base class that orchestrates the host building and service registration process.AppSurfaceStartup.RegisterDependencies: A protected seam for specialized startup types that need the module graph prepared before they build the Generic Host.ProcessUtils: The AppSurface-owned process execution surface for framework code that needs to capture stdout and stderr, non-throwing non-zero exit codes, cancellation-aware execution, and optional line-by-line logging.
Application labels and host identity
StartupContext.ApplicationName is a display label. Use it for generated documentation titles, command output, OpenAPI branding, and other user-facing product surfaces.
StartupContext.HostApplicationName is the assembly-backed identity assigned to IHostEnvironment.ApplicationName and the Generic Host applicationName setting. It defaults to the process entry assembly when one is available so cross-assembly hosts still resolve the correct static web asset manifest. When StartupContext.OverrideEntryPointAssembly is set, it uses that override assembly name instead. If no entry assembly is available, AppSurface falls back to the root module assembly as a defensive last resort.
StartupContext.EntryPointAssembly is the assembly AppSurface scans for application-owned commands, MVC application parts, Aspire components, and similar extensibility points. It defaults to the root module assembly so test runners and shared outer hosts do not accidentally scan the xUnit/VSTest process entry assembly. When StartupContext.OverrideEntryPointAssembly is set, that override applies to both discovery and host manifest identity.
Keep these values separate. ASP.NET static web assets use the host application name to find runtime manifests. Passing a custom display label such as CustomDocsHost into the host environment can make static asset requests resolve against a manifest that does not exist. When a test or custom host needs a different manifest identity, set StartupContext.OverrideEntryPointAssembly instead of overloading ApplicationName.
Environment resolution
StartupContext.EnvironmentProvider defaults to DefaultEnvironmentProvider, which keeps AppSurface module decisions aligned with the Generic Host arguments. When startup receives --environment Development or --environment=Development, StartupContext.IsDevelopment reports true before module hooks run.
If no command-line environment is supplied, AppSurface falls back to ASPNETCORE_ENVIRONMENT, then DOTNET_ENVIRONMENT, then Production. Pass a custom IEnvironmentProvider to StartupContext when a test, embedded host, or specialized runner needs a different source of truth. DefaultEnvironmentProvider.ResolveEnvironmentArgument is the shared parser for AppSurface hosts: blank, switch-like, and assignment-shaped split values are ignored, while duplicate --environment keys use the last valid value to match Microsoft configuration behavior.
Pitfalls:
- Do not read only
IHostEnvironmentwhen writing module startup decisions. Module hooks receiveStartupContextbefore the built host exists. - Do not pass
--environmentonly to the Generic Host if an AppSurface module also needs the same value. Put it inStartupContext.Args, or pass a matching customIEnvironmentProvider.
Startup dependency graph
AppSurfaceStartup registers framework dependencies and root-module dependencies exactly once per StartupContext. Standard hosts do not need to call anything directly: the registration happens during host-builder creation before module hooks and service registration run.
Specialized startup types can call the protected RegisterDependencies(StartupContext context) seam earlier when they need module-derived options before IHostBuilder.Build(). AppSurface Web uses this to resolve WebOptions.StartupTimeout before arming its startup watchdog around host creation and startup.
Call RegisterDependencies before reading StartupContext.GetDependencies() for startup-shaping decisions. Repeated calls with the same context are no-ops, but module registration is still part of startup composition, so avoid calling it from request-time code or from parallel threads.
Logging in Static Utilities
Core static utilities stay host-agnostic: they do not reach into a global logger, service provider, or ambient startup state. When a public static helper has useful diagnostics, expose an additive overload with an explicit non-null ILogger parameter and keep the existing no-logger overload silent. Private shared implementations may accept ILogger? only to avoid duplicating logic between the silent and diagnostic paths.
Use this pattern when a helper performs fallback behavior that callers may want to audit. For example, PathUtils.FindRepositoryRoot(startPath, logger) logs a warning when startPath does not exist and repository-root discovery has to continue from the nearest existing ancestor.
Prefer ordinary dependency injection for services, modules, hosted services, and application-owned classes. The optional logger pattern is only for static helpers where injecting a service instance would make the API harder to use or force unrelated callers to construct infrastructure.
Define static helper log messages with the source-generated [LoggerMessage] attribute on private static partial methods. Give each message a stable event ID, level, and template. Do not use ad hoc logger.Log... calls for new Core diagnostics when the message is part of an intentional API behavior.
Pitfalls:
- Do not create a static
LoggerFactoryinside a utility. That couples Core to a console/logging policy the host did not choose. - Do not throw only because a fallback warning was logged. Keep the documented return behavior unless the input contract itself is invalid.
- Do not swallow cleanup failures silently when a logger is available. Log them at
Debugwhen they are intentionally suppressed to preserve the primary exception or cancellation path.
Process execution utilities
ProcessUtils.ExecuteProcessAsync(...) runs an external command through CliWrap and returns a CommandResult containing ExitCode, Stdout, and Stderr. It is not a general replacement for CliWrap; it is the opinionated AppSurface process boundary for framework features that need consistent command result semantics.
Use ProcessUtils when AppSurface framework code needs this shared process policy:
- arguments are passed as an ordered
IReadOnlyList<string>and are escaped by CliWrap rather than concatenated into a shell command; workingDirectoryis applied to the launched process;- parent environment variables are inherited by default;
- stdout and stderr are captured separately;
- non-zero exit codes are returned in
CommandResult.ExitCodeinstead of throwing; - startup failures and cancellation still surface as exceptions;
streamOutput: truelogs completed stdout lines atInformationand completed stderr lines atErrorunlessstderrLogLevelSelectorremaps a specific stderr line.
Use CliWrap directly for application code or package code that needs command-specific composition, such as custom environment variable overrides, stdin piping, process pipelines, credentials, resource policies, timeouts that are not modeled as cancellation tokens, or event-stream handling. When the same advanced behavior becomes a repeated AppSurface framework concern, add it deliberately to ProcessUtils instead of reimplementing it at each call site.
Pitfalls:
StdoutandStderrare separate buffers, not a merged chronological transcript. If exact cross-stream ordering matters, use CliWrap directly and preserve the event stream yourself.streamOutput: truestill captures the full text for both streams. It is for real-time logging, not for reducing memory usage on unbounded output.- Cancellation is the timeout mechanism for this wrapper. Pass a
CancellationTokenfrom aCancellationTokenSourcewith the timeout policy you want. - Logger and
stderrLogLevelSelectorexceptions are not swallowed. A caller should treat those failures as execution failures because the returned output may be incomplete. - Keep
ProcessUtilsfocused on AppSurface framework policy. Do not add one-off knobs that only one consumer needs; use CliWrap directly at that call site.
Usage
Most users will use a more specialized package like ForgeTrust.AppSurface.Web or ForgeTrust.AppSurface.Console, which inherit from the abstractions provided here.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- CliWrap (>= 3.10.1)
- Microsoft.Extensions.Hosting (>= 9.0.6)
- Microsoft.Extensions.Logging.Console (>= 9.0.6)
- Microsoft.Extensions.Options (>= 10.0.8)
NuGet packages (6)
Showing the top 5 NuGet packages that depend on ForgeTrust.AppSurface.Core:
| Package | Downloads |
|---|---|
|
ForgeTrust.AppSurface.Web.Tailwind
ForgeTrust.AppSurface.Web.Tailwind package for AppSurface application composition. |
|
|
ForgeTrust.AppSurface.Web
ForgeTrust.AppSurface.Web package for AppSurface application composition. |
|
|
ForgeTrust.AppSurface.Caching
ForgeTrust.AppSurface.Caching package for AppSurface application composition. |
|
|
ForgeTrust.AppSurface.Console
ForgeTrust.AppSurface.Console package for AppSurface application composition. |
|
|
ForgeTrust.AppSurface.Config
ForgeTrust.AppSurface.Config package for AppSurface application composition. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.1.0-preview.4 | 279 | 5/25/2026 |
| 0.1.0-preview.3 | 98 | 5/20/2026 |
| 0.1.0-preview.2 | 101 | 5/14/2026 |