Cephalon.Behaviors.Http
Maturity:
M2· Ownership: mixed:application-managedprofile/publication activation +cephalon-managedmaterialization, governance, and runtime catalogs · Family:behaviors· See audit, matrix.
Cephalon.Behaviors.Http provides the HTTP Transport Pack for the Adaptive Behavior Topology
(ABT) system. It wires behavior topology descriptors to generic HTTP transports through 6 concrete
IHttpBehaviorBinding implementations, a lazy-init registry, and Minimal API helpers for
module-owned REST endpoints.
What it owns
Section titled “What it owns”- IHttpBehaviorBinding — thin adapter contract between generic HTTP endpoints and
BehaviorDispatcher - IHttpBehaviorBindingRegistry — frozen lookup registry for all registered transport bindings
- HttpBehaviorBindingRegistry — default
FrozenDictionary-backed registry implementation - LazyTransportBinding — deferred-init wrapper;
MapAsyncis called exactly once on first request, keeping pod startup under 100 ms - DefaultBehaviorContext —
IBehaviorContextimplementation built fromHttpContext(correlation, tenant, user, trace from headers, optionalIEventStorefrom DI) - 6 generic transport bindings — JSON-RPC 2.0, GraphQL (HTTP), GraphQL-SSE, GraphQL-WS, SSE, WebSocket
- Shared behavior API surface —
BehaviorApiSurfaceDescriptorplusBehaviorApiSurfaceRouteResolverfor canonical route-shaped generic behavior HTTP endpoints - Behavior-aware REST helpers —
MapBehaviorRestGroup(...)plusBehaviorRestEndpointGroup.MapBehaviorGet/Post/Put/Patch/Delete(...)for Minimal API-style module route groups that dispatch into behaviors - REST behavior result mapping —
BehaviorRestResponseMapperprojects raw behavior outputs and transport-neutralResult<T>outcomes into REST responses without teaching the core behavior contract about HTTP envelopes - REST behavior module base class —
RestBehaviorModuleBaseso behavior-owning REST modules can expose public endpoints without implementing multiple author-facing interfaces directly - REST behavior-module DSL —
IRestBehaviorModuleBuilderplusIRestBehaviorEndpointGroupBuilderfor one-place public REST and internal behavior ownership, compiled internally into a normalized REST projection contract before Minimal API materialization - Metadata-only REST profile contract —
BehaviorRestProfileAttribute,BehaviorRestBindingAttribute,BehaviorRestMethod,BehaviorRestMethodExtensions,BehaviorRestProfileDescriptor,BehaviorRestBindingDescriptor,BehaviorRestBindingSource, andBehaviorRestBindingSourceExtensionsfor behavior-authored candidate REST method, relative route, optional API-version hints, explicit route/query/header/body binding plans, and optional preserved implicit-query fallback intent for explicitly bound profiles that explicit module-owned shorthand such asMapProfile<TBehavior>()can consume without publishing public REST directly from behaviors; the build now rejects malformed placeholder syntax such as unbalanced{...}segments and preserved-fallback profiles that omit explicit bindings earlier, while runtime normalization still leaves final route parsing authoritative to ASP.NET Core - REST runtime ownership metadata — behavior-backed REST endpoints now publish stable
RestEndpointRuntimeMetadataKeysentries so operators can see that profile/publication activation is application-managed while ASP.NET Core materialization, candidate reconciliation, runtime catalogs, and snapshot projection are Cephalon-managed - OpenAPI enrichment — module tag names and descriptions, module-major API-version defaults
with explicit
.ApiVersion(...)and.WithOpenApiDocumentName(...)override support, best-effort XML comment summaries/descriptions for module-owned REST endpoints, and separation between public REST docs and generic adapter endpoints - REST governance diagnostics — stable
/engine/diagnosticsconvention plus information-level startup log events for shorthand governance suppression, authoring-policy suppression, precedence suppression, applied override, no-op override, and preserved binding-fallback outcomes, plus skipped explicit-governance visibility when host rules target an authored module-owned route that stayed authoritative - Optional REST response envelope —
ApiRoutes:ResultEnvelope:Enabledprojects REST success and error responses throughResultModel<T>/ResultModelErrorwithtype,status, and anerrorscollection while leaving GraphQL, JSON-RPC, SSE, and WebSocket bindings on their native protocol envelopes - Hosting —
IBehaviorCollectionBuilder.AddHttpBehaviorBindings()extension registering the generic HTTP bindings in DI
Transport bindings
Section titled “Transport bindings”| Transport ID | Binding class | Route |
|---|---|---|
http.jsonrpc | JsonRpcHttpBehaviorBinding | Canonical POST {JsonRpcPrefix}/{document}/{group}/{operation} |
http.graphql | GraphqlHttpBehaviorBinding | Canonical POST {GraphQLPrefix}/{document}/{group}/{operation} |
http.graphql-sse | GraphqlSseBehaviorBinding | Canonical POST {GraphQLSsePrefix}/{document}/{group}/{operation} |
http.graphql-ws | GraphqlWsBehaviorBinding | Canonical GET {GraphQLWsPrefix}/{document}/{group}/{operation} |
http.sse | SseBehaviorBinding | Canonical GET {SsePrefix}/{document}/{group}/{operation} |
http.ws | WebSocketBehaviorBinding | Canonical GET {WsPrefix}/{document}/{group}/{operation} |
Cephalon uses a shared BehaviorApiSurfaceDescriptor for the generic route-shaped behavior
transports. By default the API surface is derived from the behavior id, so cart.get becomes
logical group cart plus operation get, which the generic HTTP bindings project into canonical
versioned routes such as /json-rpc/v1/cart/get, /graphql/v1/cart/get,
/graphql-sse/v1/cart/get, /graphql-ws/v1/cart/get, /sse/v1/cart/get, and /ws/v1/cart/get.
The host controls those canonical prefixes through ApiRoutes:Prefixes:GraphQL,
ApiRoutes:Prefixes:JsonRpc, ApiRoutes:Prefixes:Sse, ApiRoutes:Prefixes:Ws,
ApiRoutes:Prefixes:GraphQLWs, and ApiRoutes:Prefixes:GraphQLSse, while the resolved default
version/document segment comes from ApiRoutes:DefaultBehaviorDocumentName or, when that override
is unset, the raw configured OpenApi:DefaultVersion. OpenApi:EnabledVersions and legacy
OpenApi:Documents still govern only which OpenAPI + Scalar documents get published; they do not
trim the generic behavior transport route segment.
The older /behaviors/{id} aliases are no longer part of the generated behavior HTTP surface.
Public REST uses the separate ApiRoutes:Prefixes:Rest setting through the ASP.NET Core host
adapter and MapBehaviorRestGroup(...); "" is valid and means “mount the versioned REST surface
at the root,” while null still falls back to /api.
REST ownership
Section titled “REST ownership”Cephalon keeps public REST module-owned:
- do not declare
http.restin[BehaviorAllowedTransports(...)] - do not call
ViaHttpRest()orViaHttpRest(rest => ...)inConfigureTopology(...) - prefer
RestBehaviorModuleBasewhen a module owns behaviors and exposes them publicly over REST - prefer
BehaviorModuleBasewhen a module owns behaviors but does not expose a public REST surface - keep low-level
IRestModulefor REST modules that do not dispatch into Cephalon behaviors - author public REST routes in the owning module through
ConfigureRestBehaviors(IRestBehaviorModuleBuilder behaviors) - keep behavior attributes and topology focused on interaction pattern plus non-REST transports
- keep
WithApiSurface(...)for the shared generic HTTP route surface, not for REST - if a behavior wants to describe the shipped low-ceremony REST projection paths, use
BehaviorRestProfileAttributeonly as metadata; it does not publish public REST by itself
When a behavior declares exactly one allowed pattern plus one or more allowed transports, the
runtime can synthesize that attribute-only baseline without ConfigureTopology(...). That baseline
applies to non-REST transports only. If multiple patterns are declared, startup fails fast until
another topology source selects one explicitly. For authoring convenience,
[BehaviorAllowedTransports("http.grpc")] is accepted and normalized to canonical grpc.
Shared behavior API surface
Section titled “Shared behavior API surface”When the default behavior-id -> group/operation split is not the public contract you want,
override it explicitly in ConfigureTopology(...):
public static void ConfigureTopology(IBehaviorTopologyBuilder builder){ builder.AsCqrs() .ViaHttpJsonRpc() .ViaHttpGraphQl() .ViaHttpSse() .ViaWebSocket() .WithApiSurface("catalog/items", "lookup");}That one transport-agnostic descriptor is then reused by the generic JSON-RPC, GraphQL,
GraphQL-SSE, GraphQL-WS, SSE, and WebSocket behavior bindings. Source-generated topology
descriptors honor the same WithApiSurface(...) contract, so the compile-time and
fluent-runtime paths stay aligned.
When no explicit API surface is supplied, Cephalon derives the public path deterministically from the behavior id:
cart.add-itembecomes groupcartplus operationadd-item, which projects to/json-rpc/v1/cart/add-itemcart.add-item.draftbecomes groupcart/add-itemplus operationdraft, which projects to/json-rpc/v1/cart/add-item/draft
Metadata-only REST profiles
Section titled “Metadata-only REST profiles”When a team wants lower-ceremony REST authoring, the shipped path is metadata first, not direct
public route activation. BehaviorRestProfileAttribute lets a behavior declare a candidate REST
method, relative pattern, and optional API major version for module-owned MapProfile<TBehavior>(),
MapGeneratedProfiles(...), and MapGeneratedProfileGroups(...) projections:
using Cephalon.Behaviors.Http.Abstractions;
[AppBehavior("cart.get")][BehaviorRestProfile(BehaviorRestMethod.Get, "/{cartId}", ApiVersionMajor = 2)]public sealed class GetCartBehavior : IAppBehavior<GetCartInput, Result<GetCartOutput>>{ public Task<Result<GetCartOutput>> HandleAsync( GetCartInput input, IBehaviorContext context, CancellationToken cancellationToken = default) { // behavior logic omitted }}Profiles can also carry explicit HTTP input-binding metadata when a module-owned shorthand route needs deterministic source selection:
[AppBehavior("cart.add-item")][BehaviorRestProfile(BehaviorRestMethod.Post, "/{cartId}/items", ApiVersionMajor = 2)][BehaviorRestBinding(nameof(AddToCartInput.CartId), BehaviorRestBindingSource.Route, Name = "cartId")][BehaviorRestBinding(nameof(AddToCartInput.Quantity), BehaviorRestBindingSource.Query, Name = "quantity")][BehaviorRestBinding(nameof(AddToCartInput.CorrelationId), BehaviorRestBindingSource.Header, Name = "X-Correlation-Id")][BehaviorRestBinding(nameof(AddToCartInput.Note), BehaviorRestBindingSource.Body, Name = "note")]public sealed class AddToCartBehavior : IAppBehavior<AddToCartInput, Result<AddToCartOutput>>{ // behavior logic omitted}Current profile behavior:
- the attribute is metadata only and does not publish a public REST route
- the owning module still chooses whether that behavior becomes public REST through
ConfigureRestBehaviors(...) Cephalon.Behaviors.SourceGenvalidates the core profile shape at build time and emitsGetRestProfiles()hints, including explicit binding descriptors and preserved implicit query-fallback intent when they are declared, plusGetRestProfileBehaviorTypes()hints for generated module-owned shorthandIRestBehaviorEndpointGroupBuilder.MapProfile<TBehavior>()is now the shipped low-ceremony module-owned shorthand that consumes those hints through the existing REST projection pipeline- profile consumption prefers source-generated
GetRestProfiles()hints first and falls back to the explicitly targeted behavior type’s attribute only when generated hints are unavailable - valid profiles currently require a supported REST method, a non-empty leading-slash relative
pattern such as
"/{cartId}", and a positiveApiVersionMajorwhen one is specified - when a profile declares explicit bindings,
BehaviorRestProfile(PreserveImplicitQueryFallback = true)is now the first-class author-side contract that allows a later host merge override to re-expose the remaining source query surface without turning that rewrite into a fail-fast invalid binding transition, and host governance can now also opt that same preserved source query surface into an explicit-binding shorthand candidate throughRestApi:Overrides:*:PreserveImplicitQueryFallback BehaviorRestMethodnow also exposesBehaviorRestMethodExtensionsplus the same stableget,post,put,patch, anddeletewire names that JSON serialization uses; source generation validates profile methods against that canonical vocabulary while still emitting the resolved enum member names into generatedGetRestProfiles()hints so future enum-member renames can preserve valid metadata by keeping the wire-name contract stable- runtime profile normalization and fallback conversion now echo those same canonical method wire names in their exception guidance, so operator/developer troubleshooting does not have to switch between generic runtime wording and the stable JSON/source-generation vocabulary
- the remaining method-shaped runtime guards now stay on that same vocabulary too: non-body method
body-binding rejections and unsupported REST method parser failures now also point at canonical
get/post/put/patch/deletewire names - explicit profile bindings currently support
route,query,header, andbodysources for object inputs only; build-time diagnostics now reject invalid property names, duplicate property bindings, unsupported sources, route-placeholder mismatches, and body bindings onGETorDELETE, while module-owned profile consumption still re-checks the same contract when runtime falls back to direct attribute metadata BehaviorRestBindingSourcenow also exposesBehaviorRestBindingSourceExtensionsplus the same stableroute,query,header, andbodywire names that JSON serialization uses; source generation validates profile bindings against that canonical vocabulary while still emitting the resolved enum member names into generatedGetRestProfiles()hints so future enum-member renames can preserve valid metadata by keeping the wire-name contract stable- runtime attribute-fallback and explicit binding-plan normalization now also echo those same canonical binding-source wire names in their exception guidance, so profile authoring, normalization, and troubleshooting all point at one stable source vocabulary
- when explicit bindings are present, they override the implicit merge baseline, while unbound route placeholders and body-capable request bodies can still fill remaining object properties
- explicit profiles can now also preserve the remaining implicit query-string fallback surface by
setting
BehaviorRestProfile(PreserveImplicitQueryFallback = true), but that flag requires at least one explicitBehaviorRestBindingAttribute; build-timeABT0027and runtime profile normalization both fail fast when the flag appears without bindings - explicit profiles that preserve source implicit query fallback can now also promote that same remaining query-bound property set into added route placeholders through host overrides, and the projected plus published runtime fallback-mode answer now clears once the effective binding plan fully consumes that preserved surface
- when no explicit binding plan is present, shorthand candidates still use the implicit query-plus-route merge baseline, and bounded placeholder additions can promote from that original implicit query-fallback surface; once explicit bindings exist, the stricter explicit-binding path remains in force unless the source profile or a winning host override intentionally preserves the remaining source query surface
- when a host adds only partial explicit bindings to a shorthand candidate that originally had no explicit binding plan, the remaining unbound query properties now continue to follow that original implicit query-fallback surface instead of disappearing silently
- when a body-capable shorthand candidate keeps explicit bindings but still leaves deterministic
request-body-bindable properties unbound, runtime truth now also keeps that preserved remaining
request-body fallback visible through typed
BindingFallbackMode = PreserveRemainingBodyFallback, while additivemetadata.bindingFallbackMode = preserve-remaining-body-fallbackremains compatibility-only metadata - the canonical runtime answer for preserved fallback modes now lives on the typed transport
contracts through
RestEndpointCandidateProjectionDescriptor.BindingFallbackModeandRestEndpointRuntimeDescriptor.BindingFallbackModeusingRestEndpointBindingFallbackMode, andRestEndpointBindingFallbackModeExtensions.GetWireName()/TryParseWireName(...)now provide the canonical compatibility bridge for metadata and JSON wire names; ASP.NET Core governance config now also expects those same stable wire names forBindingFallbackModesinstead of accepting enum-member aliases; additivemetadata.bindingFallbackMode = preserve-source-implicit-fallbackandmetadata.bindingFallbackMode = preserve-remaining-body-fallbackremain compatibility-only metadata - a JSON body that tries to overwrite a property reserved by an explicit non-body binding fails fast instead of silently winning or losing
- profile API-version metadata is still only a candidate endpoint version; host publication remains
governed by
OpenApi:EnabledVersions,OpenApi:DefaultVersion, and the legacy document allow-list settings
REST governance diagnostics
Section titled “REST governance diagnostics”When Cephalon.Behaviors.Http is active, /engine/diagnostics now also publishes source
Cephalon.Behaviors.Http with stable event ids 5200-5206:
5200— a shorthand candidate was suppressed by a configured governance rule and now echoes the decisive suppression selection-basis wire name beside the winning suppression id5201— a shorthand candidate lost publication because a higher-precedence authoring style won5202— a matched shorthand override materially changed the published runtime answer and now echoes the decisive override selection-basis wire name plus the selected and applied override action-kind wire names beside the winning override id5203— a matched shorthand override became a runtime no-op after truth reconciliation and now still echoes the decisive override selection-basis wire name plus the selected and applied override action-kind wire names so no-op winners remain explicit5204— a shorthand candidate preserved a typed binding fallback mode while partial explicit override reconciliation ran, including both preserved source implicit-query fallback and preserved remaining request-body fallback, and startup logging now echoes the same stable fallback-mode wire name the runtime catalog and compatibility metadata use5205— a shorthand candidate was suppressed by authoring policy, includingdisallowed-authoring-style,not-allowed-authoring-style, andpreferred-authoring-style-selected5206— an explicit module-DSL candidate skipped targeted host suppression and/or override rules because its original projection did not allow host governance
Those events are emitted during startup/materialization when information-level logging is enabled.
For full hosted MapCephalon() paths, published-candidate logging reconciles against the actual
post-materialization endpoint answer first, so metadata-only or reorder-only no-op overrides keep
MatchedOverrideIds visible without falsely logging an applied override, and governance-ineligible
explicit routes now log the skipped host-rule ids instead of silently looking like selector misses.
Event 5200 now also reuses the same SuppressionSelectionBasis wire name the runtime catalog
publishes, while the override-applied and override-no-op events reuse the same
OverrideSelectionBasis, ActionKinds, SelectedOverrideActionKinds, and
AppliedOverrideActionKinds story the runtime catalogs expose, using the stable wire names from
RestEndpointGovernanceRuleSelectionBasisExtensions and
RestEndpointOverrideActionKindExtensions. Event 5204 now also reuses
RestEndpointBindingFallbackModeExtensions.GetWireName() so startup diagnostics, JSON
serialization, runtime catalogs, and additive compatibility metadata all speak the same stable
fallback-mode wire names.
Registration
Section titled “Registration”services.AddCephalon(config, engine => engine .AddBehaviors(behaviors => behaviors .Register<PlaceOrderBehavior>(b => b.AsCqrs().ViaHttpJsonRpc().ViaHttpSse()) .AddHttpBehaviorBindings() ));Behavior-aware REST endpoints
Section titled “Behavior-aware REST endpoints”Behaviors no longer activate REST through annotations or topology alone. Instead, modules map REST
endpoints explicitly through the Minimal API helper layer while still dispatching through
BehaviorDispatcher.
When a module wants a public REST surface, keep ownership and REST mapping together explicitly:
public sealed class CartModule : RestBehaviorModuleBase{ public override ModuleDescriptor Descriptor => DescriptorInstance;
public override void ConfigureRestBehaviors(IRestBehaviorModuleBuilder behaviors) { var group = behaviors.Group("/showcase/cart"); group.MapGet<GetCartBehavior>("/{cartId}"); group.MapPost<AddToCartBehavior>("/{cartId}/items"); group.MapDelete<RemoveFromCartBehavior>("/{cartId}/items/{productId}"); group.MapPost<CheckoutCartBehavior>("/{cartId}/checkout");
behaviors.Internal<RepriceCartBehavior>(); }}When the behavior already carries a REST profile and the module wants the lower-ceremony path, the same module-owned DSL can consume that metadata explicitly:
public sealed class CartModule : RestBehaviorModuleBase{ public override ModuleDescriptor Descriptor => DescriptorInstance;
public override void ConfigureRestBehaviors(IRestBehaviorModuleBuilder behaviors) { var group = behaviors.Group("/showcase/cart") .WithTagName("Cart API");
group.MapProfile<GetCartBehavior>(); group.MapProfile<AddToCartBehavior>(); group.MapProfile<RemoveFromCartBehavior>(); group.MapProfile<CheckoutCartBehavior>();
behaviors.Internal<RepriceCartBehavior>(); }}When the module wants to publish every profiled behavior beneath one owned public prefix, it can opt into the generated shorthand explicitly:
public sealed class CartModule : RestBehaviorModuleBase{ public override ModuleDescriptor Descriptor => DescriptorInstance;
public override void ConfigureRestBehaviors(IRestBehaviorModuleBuilder behaviors) { behaviors.Group("/showcase/cart") .WithTagName("Cart API") .MapGeneratedProfiles();
behaviors.Internal<RepriceCartBehavior>(); }}MapGeneratedProfiles() derives the behavior-id prefix from the route-group prefix, so the example
above selects behaviors whose ids start with showcase.cart. If a module wants a different
selection rule, use MapGeneratedProfiles("custom.prefix") explicitly.
If the module wants to start from the behavior-id prefix instead, use
behaviors.GroupFromBehaviorIdPrefix("showcase.cart").MapGeneratedProfiles();. Cephalon derives
the public route-group prefix as /showcase/cart and still keeps generated publication explicit
and module-owned.
When one module wants to fan one generated root prefix out into several owned public route groups,
it can now stay on the same generated shorthand path through MapGeneratedProfileGroups(...):
public sealed class GeneratedCatalogModule : RestBehaviorModuleBase{ public override ModuleDescriptor Descriptor { get; } = new( "showcase.generated", "Generated Catalog Module", "Publishes generated REST profiles across several derived route groups.", version: "1.0.0");
public override void ConfigureRestBehaviors(IRestBehaviorModuleBuilder behaviors) { behaviors.MapGeneratedProfileGroups( "showcase.generated", group => group .WithTagName("Generated Catalog API") .WithHostGovernanceScope("generated-catalog")); }}Cephalon groups matching behavior ids by their parent prefix before publication, so
showcase.generated.orders.lookup and showcase.generated.orders.create share
/showcase/generated/orders, while showcase.generated.inventory.lookup lands beneath
/showcase/generated/inventory. The optional callback applies the same group-level conventions to
each derived route group, and the generated endpoints still publish through the existing
behavior-module-generated authoring-style and runtime-catalog pipeline instead of inventing a
second generated publication source. Hosts can now also target those same grouped branches by exact
behavior id or by subtree BehaviorIdPrefixes such as showcase.generated.orders, so one
governance rule can suppress or override one generated group without enumerating every exact
generated candidate id.
When each derived route group needs different conventions, use the derived-prefix-aware overload:
behaviors.MapGeneratedProfileGroups( "showcase.generated", (derivedBehaviorIdPrefix, group) => { if (string.Equals(derivedBehaviorIdPrefix, "showcase.generated.orders", StringComparison.Ordinal)) { group.ApiVersion(2) .WithTagName("Generated Orders API") .WithHostGovernanceScope("generated-orders"); } else if (string.Equals(derivedBehaviorIdPrefix, "showcase.generated.inventory", StringComparison.Ordinal)) { group.WithTagName("Generated Inventory API") .WithHostGovernanceScope("generated-inventory"); } });The callback now receives each derived generated behavior-id prefix before its profiles are mapped, so one owning module can stamp different version, tag, or governance-scope conventions per branch without restating every derived group manually. Generated shorthand already participates in host governance by default, so this overload is mainly about per-branch projection conventions and stable original-projection scope truth rather than opt-in governance.
When a project wants the same module-owned REST behavior without creating a dedicated module class, the host can still register an inline module explicitly:
engine.AddRestBehaviorModule<GetCartBehavior>( "showcase.cart", "Cart Module", "Publishes the cart public REST surface without a dedicated module class.", behaviors => behaviors.Group("/showcase/cart") .WithTagName("Cart API") .MapGeneratedProfiles("showcase.cart"), version: "1.0.0");AddRestBehaviorModule<TMarker>(...) remains module-owned: it materializes a real Cephalon module,
feeds the same RestBehaviorModuleBuilder projection pipeline, and still never publishes public
REST from [AppBehavior] alone. The marker type should come from the same behavior assembly that
owns the published behaviors, especially when the inline module uses MapGeneratedProfiles(...),
because Cephalon resolves generated REST profile hints from that marker assembly. Use one stable
marker type per inline module; if a module needs richer lifecycle hooks, extra services, or more
advanced manual endpoints, prefer a dedicated RestBehaviorModuleBase subclass instead. The
string-based overload keeps the common path low-ceremony; the ModuleDescriptor overload remains
the advanced path when an inline module needs explicit dependency, tag, or metadata declarations.
For the common generated-profile case where the route group should mirror the behavior-id prefix, the host can now use the lower-ceremony inline helper:
engine.AddGeneratedRestBehaviorModule<GetCartBehavior>( "showcase.cart", "Cart Module", "Publishes the cart public REST surface through the generated inline helper.", group => group.WithTagName("Cart API"), version: "1.0.0");AddGeneratedRestBehaviorModule<TMarker>(...) still creates a real module, still maps through the
same generated-profile projection and runtime-catalog pipeline, and still never publishes public
REST from [AppBehavior] alone. The common string-based overload derives the generated behavior-id
prefix from the inline module id, while the ModuleDescriptor and explicit behaviorIdPrefix
overloads remain available when the inline module needs richer metadata or when module identity and
generated ownership prefix should differ. Keep AddRestBehaviorModule<TMarker>(...) when the route
group should not mirror the behavior-id prefix or when the inline module needs more than one group.
When one inline module wants one generated root prefix to fan out into several owned public route groups, the host can now stay on that same grouped generated path through the matching inline helper:
engine.AddGeneratedRestBehaviorModuleGroups<GetCatalogOrderBehavior>( "showcase.generated", "Generated Catalog Module", "Publishes generated REST profiles across several derived route groups through the inline helper.", group => group .WithTagName("Generated Catalog API") .WithHostGovernanceScope("generated-catalog"), version: "1.0.0");AddGeneratedRestBehaviorModuleGroups<TMarker>(...) still creates a real module, still delegates
to MapGeneratedProfileGroups(...), and still keeps grouped generated publication on the same
behavior-module-generated candidate, publication-group, governance, and runtime-catalog path.
The common overload derives the grouped generated behavior-id root prefix from the inline module id,
while the explicit behaviorIdPrefix and ModuleDescriptor overloads remain available when inline
module identity, grouped generated ownership, or inline metadata should differ. Grouped generated
prefix validation now also fails fast consistently for both dedicated-module and inline-helper paths
when the supplied dot-separated prefix contains empty segments.
When the inline helper also needs different conventions per derived group, use the matching derived-prefix-aware overload:
engine.AddGeneratedRestBehaviorModuleGroups<GetCatalogOrderBehavior>( "showcase.generated", "Generated Catalog Module", "Publishes generated REST profiles across several derived route groups through the inline helper.", (derivedBehaviorIdPrefix, group) => { if (string.Equals(derivedBehaviorIdPrefix, "showcase.generated.orders", StringComparison.Ordinal)) { group.ApiVersion(2) .WithTagName("Generated Orders API") .WithHostGovernanceScope("generated-orders"); } else if (string.Equals(derivedBehaviorIdPrefix, "showcase.generated.inventory", StringComparison.Ordinal)) { group.WithTagName("Generated Inventory API") .WithHostGovernanceScope("generated-inventory"); } }, version: "1.0.0");That keeps the same inline-module ownership story, but now the callback can branch on
showcase.generated.orders versus showcase.generated.inventory before the generated profiles are
materialized.
Current helper behavior:
- keeps REST route shape in the host-adapter layer instead of overloading behavior attributes with HTTP-specific concerns
- lets behaviors carry candidate REST projection metadata without turning that metadata into a public route by itself
- gives behavior-owning REST modules both a dedicated base class and a low-code
AddRestBehaviorModule<TMarker>(...)host helper instead of forcing authors to implementIBehaviorOwnerModuleplusIRestModulemanually - adds
GroupFromBehaviorIdPrefix(...)plusAddGeneratedRestBehaviorModule<TMarker>(...)for the common generated-profile path where the route-group prefix, generated behavior-id prefix, and inline module id should mirror one another while the explicitbehaviorIdPrefixandModuleDescriptoroverloads stay available when they should differ or the inline module needs richer metadata - treats the REST DSL as the primary authoring path, so public routes also imply module ownership
- compiles author-facing REST group and endpoint declarations into a reusable internal projection model before the ASP.NET Core adapter materializes route groups and handlers
- keeps
Internal<TBehavior>()available for internal-only behaviors or behaviors that will be exposed through custom/manual endpoints - lets straightforward hosts register an explicit module-owned REST surface inline without creating a dedicated module class, while still reusing the same normalized projection and runtime-catalog path
- adds
MapProfile<TBehavior>()as an explicit module-owned shorthand that consumes the behavior profile’s method, relative pattern, optional candidate API version, any explicit binding descriptors, and optional preserved implicit query-fallback intent for explicitly bound profiles - adds
MapGeneratedProfiles()andMapGeneratedProfiles(string)as explicit module-owned low-code shorthands that publish every matching profiled behavior beneath one owned route group - adds
MapGeneratedProfileGroups(string), the shared-group-configuration overload, the derived-prefix-aware overload, and matchingAddGeneratedRestBehaviorModuleGroups<TMarker>(...)helpers when one generated root prefix should fan out into several derived owned route groups while preserving the same generated shorthand runtime truth - derives the default generated-selection prefix from the route-group path by trimming slashes and
replacing
/separators with., while still allowing an explicit behavior-id prefix override - derives the common generated route-group path from a dot-separated behavior-id prefix when
authors use
GroupFromBehaviorIdPrefix(...)orAddGeneratedRestBehaviorModule<TMarker>(...) - prefers source-generated profile hints and falls back only to the explicitly targeted behavior type instead of broad assembly reflection
- prefers source-generated
GetRestProfiles()plusGetRestProfileBehaviorTypes()hints for generated shorthand and falls back only to a bounded scan of the explicit owning module assembly when generated type hints are unavailable - lets the inline helper point generated-profile discovery at the correct behavior assembly through
its
TMarkertype instead of assuming the reusable helper implementation assembly owns those behaviors - keeps explicit route bindings honest by requiring the declared binding name to match a placeholder present in the profile route template
- lets explicit group
.ApiVersion(...)override profile-declared candidate versions, while conflicting profile-declared versions in the same group fail fast until the module resolves them - keeps that explicit group
.ApiVersion(...)authoritative for host-level version rewrites even when the same shorthand candidate later matchesRestApi:Overrides, while shorthand method, bounded route-group-prefix, and constrained pattern overrides can still apply - keeps runtime publication on the same module-owned path with
sourceKind = module-dsl, while/engine/rest-endpointsexposes first-classAuthoringStyle = behavior-module-profilefor the profile shorthand path,behavior-module-generatedfor the generated shorthand path,behavior-module-dslfor the fully explicit path, exposes first-classRouteGroupPrefixplusRelativePatternfor the resolved published group boundary, exposes first-class nullableBehaviorTypefor the concrete behavior implementation identity, exposes first-class nullableSourceIdfor published source identity, keeps additivemetadata.authoringStyle,metadata.routeGroupPrefix,metadata.relativePattern,metadata.behaviorType, andmetadata.sourceIdonly for compatibility, now lets shorthand ASP.NET Core materialization readProjectedEndpoint.RouteGroupPrefixdirectly instead of parsing compatibility metadata, and publishes first-classBindingDescriptorsdata for profile-driven explicit binding plans - dispatches through
BehaviorDispatcherusing Minimal API handlers - lets behaviors return raw
TOutputor transport-neutralResult<TOutput>values - uses the implicit route/query/body merge baseline only when no explicit profile bindings are
present; profile-driven bindings switch to the descriptor-aware override model instead, unless the
profile explicitly preserves source implicit query fallback through
BehaviorRestProfile(PreserveImplicitQueryFallback = true), in which case later host overrides can still promote only that remaining preserved query surface into added placeholders while the runtime fallback-mode answer stays truthful to the effective binding plan - uses the owning module display name as the OpenAPI tag
- lets the module override the published tag name and tag description through
.WithTagName(...)and.WithTagDescription(...) - defaults the tag description from the module XML
<summary>plus<remarks>when XML docs exist, falling back toModuleDescriptor.Description - defaults newly mapped endpoints to the owning module descriptor major version when one is
available, so a module declared as
1.0.0automatically joins thev1document and gets a/v1route prefix even without.ApiVersion(1) - keeps
.ApiVersion(major)as the explicit override when a module needs a public API version that differs from the module package major - lets modules pin a published OpenAPI document name independently from the
/v{major}route segment through.WithOpenApiDocumentName(...)when a stable named document such aspublicshould remain authoritative - prefixes the mapped REST route group with
/v{major}for the resolved API major version, so hosts expose paths such as/api/v1/showcase/cart/{cartId} - uses the resolved API major version as the operation-name version segment, falling back to the
owning module descriptor major version before the default
v1document name - reads XML comments from the module and behavior assemblies when available so ASP.NET Core OpenAPI + Scalar can show summaries and descriptions without extra boilerplate
- maps behavior
<summary>to the OpenAPI operation summary and behavior<remarks>to the OpenAPI operation description so Scalar does not repeat the same text twice - lets modules declare candidate OpenAPI document membership through
.ApiVersion(...),.WithOpenApiDocumentName(...), or the owning module major version, while the host-levelOpenApi:EnabledVersionslist still decides which versioned docs are actually published and legacyOpenApi:Documentsremains the named-document publication contract - expects
/scalarto redirect to the default canonical document such as/scalar/v1, while/scalar/remains available for multi-document flows and hash-based selections are normalized back into pinned versioned links - inherits the host-injected Scalar selector, so when more than one published document exists the UI offers a version dropdown driven by the enabled-document allow-list and default-document choice
- lets hosts move the OpenAPI JSON endpoint, Scalar UI base path, and REST host prefix through
OpenApi:RoutePattern,OpenApi:Scalar:RoutePrefix, andApiRoutes:Prefixes:Rest - still interoperates with legacy
OpenApi:DocumentsandOpenApi:DefaultDocumentsettings when a host needs custom named docs instead of major-version documents - keeps module-owned REST routing distinct from the generic behavior transport surface; side-by-side major-version behavior identities still require a later behavior-identity and transport-surface rework
- rejects module-owned REST mappings that target a behavior explicitly owned by another module
- keeps
MapAdditionalEndpoints(...)as the advanced/manual Minimal API escape hatch for REST modules that need extra routes beyond the default behavior DSL, while still flowing those manual routes into/engine/rest-endpointsand the shared duplicate-route guard - if the same behavior is mapped through both explicit module DSL and shorthand publication, the explicit DSL route now wins by default and the lower-precedence shorthand candidate is suppressed instead of publishing side by side
- if the same behavior is mapped through both
MapProfile<TBehavior>()and generated shorthand throughMapGeneratedProfiles(...)orMapGeneratedProfileGroups(...), the explicit per-behaviorMapProfile<TBehavior>()route wins by default and the generated candidate is suppressed - ASP.NET Core hosts can now also suppress descriptor-backed shorthand candidates through
RestApi:Suppressions, which runs before precedence resolution, records the governing rule id on the suppressed candidate throughSuppressedBySuppressionId, and intentionally leaves explicit module DSL or manual module-owned REST endpoints untouched by default; an explicit module-DSL route group can opt into that governance boundary throughAllowHostGovernance(), but host rules still have to target authoring stylebehavior-module-dslexplicitly before they can suppress it, while generated grouped shorthand can now be targeted either by exactBehaviorsor by subtreeBehaviorIdPrefixes - ASP.NET Core hosts can now also override the effective API major version, HTTP method, or
bounded published route-group prefix, constrained relative route pattern, required capability
boundary, ordered required feature-flag boundary, or explicit binding
plan for descriptor-backed shorthand candidates through
RestApi:Overrides, which now supportsApiVersionMajor,OpenApiDocumentName,Method,RouteGroupPrefix,Pattern,RequiredCapabilityKey,ClearRequiredCapability,RequiredFeatureFlagIds,ClearRequiredFeatureFlags,Bindings,RemovedBindingProperties, shorthand binding resets throughClearBindings, typedBindingMode, host-governed preserved query fallback throughPreserveImplicitQueryFallback, shorthand endpoint metadataEndpointName,Summary, andDescription, OpenAPI tag-name rewrites throughTagName, plus metadata clearsClearEndpointName,ClearSummary, andClearDescription, records the applied rule id throughAppliedOverrideIdwhen the selected rule materially changes the effective answer, rewrites the shorthand candidate’s/v{major}route segment and OpenAPI document name together when version changes, keeps the mapped endpoint method aligned when method changes, keeps the published route-group boundary aligned whenRouteGroupPrefixchanges, keeps the mapped endpoint route aligned when pattern changes, keeps actual ASP.NET Core endpoint metadata plus/engine/rest-endpointsaligned when capability-boundary or endpoint-metadata values change or clear, keeps effective tag truth aligned whenTagNamechanges, applies explicit binding overrides in either default wire-name-onlyreplace-explicitmode ormerge-explicitproperty-patch-and-withdraw mode while leaving unbound route placeholders and remaining request-body fields available for deterministic fallback, now also keeps omitted binding modes visible as runtime JSONunspecifiedon/engine/rest-endpoint-overridesplussnapshot.RestEndpointOverrideswhile leaving host config explicit-mode-only, and now allows configured rules to publish their declared effect dimensions through typedActionKinds, and now keeps the winning rule’s declared-versus-effective action dimensions visible on both runtime candidates and published endpoints throughSelectedOverrideActionKindsplusAppliedOverrideActionKindsso no-op winning overrides remain explicit without falsely claiming a material runtime rewrite, now allows placeholder renames when the effective explicit route-binding plan covers the renamed placeholder set exactly, now also allows placeholder removals when the original projection already exposes explicit route-binding coverage for the original placeholder set and the effective explicit binding plan keeps every affected original route-bound property explicitly bound, now also allows placeholder additions when the effective explicit route-binding plan covers the full final placeholder set and every newly route-bound property was either already explicitly bound in the original projection, forPOST/PUT/PATCHalready part of the original deterministic remaining-body fallback surface, or for shorthand candidates with no explicit binding plan already part of the original implicit query-fallback surface, or for explicit-binding shorthand candidates whose remaining source query surface is intentionally preserved by the source profile or by a winningPreserveImplicitQueryFallbackhost override;RouteGroupPrefixstays bounded beneath the active REST root with no placeholders and no implicit API-version drift, materialization now splits effective shorthand route groups when only some candidates in one authored group are remapped or retagged, using the effective route-group prefix plus tag to keep actual endpoint tag metadata aligned with runtime truth, merge-time removals must still target properties the source shorthand already bound explicitly, no-explicit-plan shorthand candidates now also preserve their remaining implicit query-fallback surface when a host adds only partial explicit bindings, the typed runtime and projection descriptors now keep that preserved mode visible throughBindingFallbackModevalues backed byRestEndpointBindingFallbackMode, additivemetadata.bindingFallbackMode = preserve-source-implicit-fallbackandmetadata.bindingFallbackMode = preserve-remaining-body-fallbackremain compatibility-only metadata, merge-mode removals on non-body-capable methods now also fail fast when they would stop explicitly binding a source query-bound property unless that property still belongs to a profile that intentionally preserved source implicit query fallback, a winning host override that setsPreserveImplicitQueryFallback = true, orClearBindings = true; that clear can now also discard the source shorthand explicit binding plan entirely and return the candidate to the implicit route/query/body baseline, but that clear fails fast if the effective route placeholders would only remain satisfiable through removed explicit route-binding aliases, and explicit module DSL/manual routes plus shorthand groups with explicit.ApiVersion(...)remain authoritative for version selection by default; explicit module-DSL route groups enter that override boundary only when the owning group callsAllowHostGovernance()and the matching host rule explicitly targets authoring stylebehavior-module-dsl
REST runtime catalog and collision guard
Section titled “REST runtime catalog and collision guard”When Cephalon materializes module-owned REST onto ASP.NET Core, whether through the behavior DSL or through explicit manual module-owned routes, the host now also publishes the resolved public REST answer through:
IRestEndpointRuntimeCatalogGET /engine/rest-endpointsGET /engine/rest-endpoints/{restEndpointId}RuntimeIntrospectionSnapshot.RestEndpoints
Each catalog entry now carries the resolved public route shape rather than only the authoring-time
DSL input, including the final HTTP method, final route pattern, source kind, owning module id and
version when known, behavior id when the route dispatches through a Cephalon behavior, published
OpenAPI document name, resolved API major version, tags, first-class request-binding descriptors
when an explicit profile-driven plan exists, first-class AuthoringStyle, first-class
RouteGroupPrefix plus RelativePattern, first-class nullable BehaviorType, first-class
nullable SourceId, nullable CandidateId, nullable OriginalProjection, and nullable
OriginalEndpointName / OriginalSummary / OriginalDescription when the published endpoint
comes from the module-owned behavior projection pipeline instead of a manual or behavior-helper
route. That published OriginalProjection keeps the original shorthand method, route,
document-version, tag name, and binding-plan truth visible directly on the final runtime endpoint,
while the original-metadata trio keeps the original shorthand endpoint name plus summary/description
visible beside the final effective endpoint metadata, so operators no longer need a
candidate-catalog join or behavior-doc fallback just to compare original-versus-effective
publication. When a host override clears one of those endpoint-metadata fields, the effective
ASP.NET Core endpoint metadata and runtime-catalog answer now stay intentionally empty while the
original-metadata trio still preserves the source shorthand lineage. When a host override retags one
candidate, the effective tag now flows through actual ASP.NET Core endpoint tag metadata plus the
candidate and published runtime catalogs while OriginalProjection.TagName preserves the source
shorthand tag; if only some candidates in one authored group are retagged, materialization splits
the effective published groups so runtime truth and actual tag metadata stay aligned.
That same split now applies to binding fallback truth too: OriginalProjection.BindingFallbackMode
stays the authored-source answer, while ProjectedEndpoint.BindingFallbackMode and the final
published endpoint may surface PreserveSourceImplicitFallback only when a winning override
legitimately re-exposes preserved remaining query surface after explicit binding withdrawal.
OriginalProjection now also carries AllowsHostGovernance, so both candidate and published
runtime answers show whether host suppression or override rules were even allowed to participate.
Shorthand candidates publish that flag as true by default, while explicit module-DSL routes
publish true only when the owning route group explicitly called AllowHostGovernance().
When an explicit module-DSL route stays authoritative because the group did not opt in, the same
candidate and published endpoint answers now also keep ordered SkippedSuppressionIds and
SkippedOverrideIds visible. That makes targeted host rules observable without pretending they
matched or won governance for a route that never entered the governable surface.
The same runtime answer now has a companion candidate catalog for precedence visibility:
IRestEndpointCandidateRuntimeCatalogGET /engine/rest-endpoint-candidatesGET /engine/rest-endpoint-candidates/{candidateId}RuntimeIntrospectionSnapshot.RestEndpointCandidates
The same host now also publishes the grouped publication answer for those same behavior-backed candidates:
IRestEndpointPublicationGroupRuntimeCatalogGET /engine/rest-endpoint-publication-groupsGET /engine/rest-endpoint-publication-groups/{behaviorId}RuntimeIntrospectionSnapshot.RestEndpointPublicationGroups
The same host now also publishes the rule-centric behavior-level authoring-policy answer:
IRestEndpointAuthoringPolicyRuntimeCatalogGET /engine/rest-endpoint-authoring-policiesGET /engine/rest-endpoint-authoring-policies/{behaviorId}RuntimeIntrospectionSnapshot.RestEndpointAuthoringPolicies
That policy surface complements grouped publication answers by keeping explicitly configured
RestApi:AuthoringPolicies:{behaviorId} entries visible even when no current shorthand candidate
matches one behavior boundary, while also separating CandidateIds, RetainedCandidateIds,
PublishedCandidateIds, PrecedenceSuppressedCandidateIds,
GovernanceSuppressedCandidateIds, and grouped SuppressionSummaries in one behavior-level
runtime answer. That same rule-centric answer now also carries AuthoringStyleSummaries, so one
payload can partition those candidate, retained, published, precedence-suppressed,
governance-suppressed, and authoring-policy-suppressed outcomes by normalized authoring style while
still leaving explicitly configured-but-unmatched policies visible with an empty style-summary set.
The same authoring-policy payload now also keeps HostGovernanceEligibleCandidateIds,
HostGovernanceIneligibleCandidateIds, SkippedSuppressionIds, SkippedOverrideIds,
GovernanceSuppressionSummaries, GovernanceOverrideSummaries, SkippedSuppressionSummaries,
and SkippedOverrideSummaries visible at both the behavior level and inside each style summary,
so explicit ownership that never entered host governance remains visible without reopening
publication-group answers first and operators can inspect matched-versus-suppressed,
selected-versus-applied, and skipped-rule candidate mappings from that same authoring-policy
payload.
Those same candidates now also drive the inverse per-rule runtime answer for host governance:
/engine/rest-endpoint-suppressions now shows MatchedCandidateIds,
SuppressedCandidateIds, SkippedCandidateIds, SelectionBases, and
SelectionBasisSummaries per suppression rule, while
/engine/rest-endpoint-overrides now shows MatchedCandidateIds, SelectedCandidateIds,
AppliedCandidateIds, SkippedCandidateIds, SelectionBases, SelectionBasisSummaries,
SelectedActionKinds, SelectedActionKindSummaries, AppliedActionKinds, and
AppliedActionKindSummaries per override rule, so the operator can move between
candidate-centric, behavior-grouped, and rule-centric views without losing one shared runtime
truth or re-bucketing candidate provenance by hand.
Each grouped publication answer now also carries AuthoringStyleSummaries, which summarizes per
normalized authoring style the participating candidate ids, precedence ranks, published
candidates, and precedence-versus-governance suppression buckets derived from the same grouped
candidate truth. Those grouped answers now also keep
HostGovernanceEligibleCandidateIds, HostGovernanceIneligibleCandidateIds,
SkippedSuppressionIds, SkippedOverrideIds, SkippedSuppressionSummaries, and
SkippedOverrideSummaries visible at both the grouped behavior level and inside each
authoring-style summary, so explicit module-DSL ownership that stayed out of host governance is
still visible without drilling into each candidate and operators can see which candidate ids each
skipped host rule actually targeted.
Each grouped publication answer now also carries AuthoringPolicy as typed
RestEndpointPublicationGroupAuthoringPolicyDescriptor data. That policy surface distinguishes the
implicit default single-winner boundary from an explicit
RestApi:AuthoringPolicies:{behaviorId} configuration and keeps
AllowMultiplePublishedCandidates plus preferred/allowed/disallowed authoring-style intent
visible. That same policy surface now also changes actual publication behavior for grouped
shorthand candidates: when AllowMultiplePublishedCandidates = true, lower-precedence unsuppressed
shorthand candidates can remain published beside the default winner as long as the resolved public
route answers stay distinct. When those co-published candidates would otherwise reuse the same
effective endpoint name, candidate resolution now disambiguates that name deterministically while
preserving OriginalEndpointName as source shorthand lineage. Preferred/allowed/disallowed
authoring-style fields now also suppress shorthand candidates when they fall outside the configured
policy, while explicit module DSL publication remains authoritative and runtime truth keeps those
authoring-policy outcomes distinct from governance suppression and precedence suppression. The same
operator-facing candidate payloads now also keep SuppressedByAuthoringPolicyKind on canonical
disallowed-authoring-style, not-allowed-authoring-style, and
preferred-authoring-style-selected wire names across /engine/rest-endpoint-candidates,
candidate-by-id payloads, and snapshot.RestEndpointCandidates.
Grouped publication answers now also keep that same policy truth summarized by suppression kind
through AuthoringPolicySuppressionSummaries at both the behavior-group level and inside each
AuthoringStyleSummaries entry, so /engine/rest-endpoint-publication-groups plus
snapshot.RestEndpointPublicationGroups can show whether a shorthand candidate was filtered by a
disallowed style, a not-allowed style, or a preferred-style winner without re-reading each
candidate payload individually.
The host now also publishes the configured shorthand-suppression rules themselves through:
IRestEndpointSuppressionRuntimeCatalogGET /engine/rest-endpoint-suppressionsGET /engine/rest-endpoint-suppressions/{suppressionId}RuntimeIntrospectionSnapshot.RestEndpointSuppressions
The host now also publishes the configured shorthand-override rules themselves through:
IRestEndpointOverrideRuntimeCatalogGET /engine/rest-endpoint-overridesGET /engine/rest-endpoint-overrides/{overrideId}RuntimeIntrospectionSnapshot.RestEndpointOverrides
Candidate entries answer the original shorthand projection shape through
RestEndpointCandidateRuntimeDescriptor.OriginalProjection, the final effective mapped answer
through ProjectedEndpoint, that same effective answer’s original shorthand endpoint metadata
through ProjectedEndpoint.OriginalEndpointName / OriginalSummary / OriginalDescription,
authoring style, precedence rank, published versus suppressed status, and when suppression occurs
the winning candidate id plus an operator-facing suppression reason.
Candidate ids now resolve from that original shorthand projection before host-level overrides are
applied, while ProjectedEndpoint.Id continues to identify the final effective mapped endpoint
shape, and published behavior-backed endpoints now also keep that same join visible directly through
RestEndpointRuntimeDescriptor.CandidateId, while published endpoints of every authoring path now
also expose first-class RestEndpointRuntimeDescriptor.AuthoringStyle so consumers no longer have
to parse metadata.authoringStyle to recover published-endpoint authorship. Projected shorthand
endpoints now also keep endpoint names plus summary/description metadata aligned with the final
published runtime endpoint conventions, including XML-derived behavior docs when they exist and
module-description fallback when they do not, and published candidate registration now reconciles
capability-only, endpoint-metadata-only, and tag-only no-op matches against the actual mapped
endpoint so MatchedOverrideIds can still show the winning rule without forcing AppliedOverrideId
to pretend a same-key rewrite, no-op clear, or same-value metadata or tag rewrite changed the
published boundary. The final published /engine/rest-endpoints plus
snapshot.RestEndpoints answer now also keeps that same ordered MatchedOverrideIds set visible
directly, so operators no longer need a candidate join to see matched shorthand override rules on a
live endpoint. When more than one suppression
or override rule matches the same shorthand candidate, that same runtime entry now also keeps the
full ordered match set visible through MatchedSuppressionIds and MatchedOverrideIds before
SuppressedBySuppressionId or AppliedOverrideId identifies the selected winner. Today that
surface covers the normalized module-owned behavior projection path, including explicit module DSL
mappings,
MapProfile<TBehavior>() shorthand consumption, and
MapGeneratedProfiles(...) / MapGeneratedProfileGroups(...) shorthand consumption.
Behavior-backed entries also carry stable ownership metadata through
RestEndpointRuntimeMetadataKeys:
restPublicationActivationOwnership = application-managedmeans a module, inline helper, or host explicitly opted into a public REST boundary;[AppBehavior]andBehaviorRestProfileAttributestill do not publish public REST by themselvesrestMaterializationOwnership = cephalon-managedmeans Cephalon owns projection materialization, ASP.NET Core route registration, candidate reconciliation, runtime catalogs, and snapshot projection after that explicit activationrestProfileMetadataOwnership = application-managedappears on profile/generated shorthand endpoints to show that behavior-authored profile metadata is an input contract, not an automatic publication authorityrestPublicationActivationModerecords the explicit activation path, for exampleexplicit-map-profile,explicit-map-generated-profiles,explicit-module-dsl, orexplicit-behavior-helper
That split is the maturity contract for this package: profile metadata stays intentionally non-publishing, while module-owned activation flows through Cephalon-managed materialization and operator-visible runtime truth.
Publication-group entries now answer that same runtime truth one behavior at a time: the ordered candidate set, the published candidate ids that survived with the winning precedence rank, the precedence-suppressed candidate ids, and the governance-suppressed candidate ids. That grouped surface keeps multi-source shorthand stories readable even when one behavior has explicit DSL, profile shorthand, generated shorthand, or selector-scoped governance interacting at once.
When suppression comes from host governance instead of precedence, the runtime now uses
SuppressedBySuppressionId rather than SuppressedByCandidateId so operators can see that a
configuration rule hid the candidate instead of another candidate winning. When more than one
suppression rule matched, the runtime also keeps the full specificity-ordered match set visible
through MatchedSuppressionIds.
Those same publication-group answers now also expose grouped GovernanceSuppressionSummaries and
GovernanceOverrideSummaries at both the behavior-group level and inside each
AuthoringStyleSummaries entry. Suppression summaries keep matched-versus-actually-suppressed
candidate ids visible per host rule, while override summaries keep matched, selected, and applied
candidate ids visible per rule so no-op winning overrides stay explicit when SelectedOverrideId
wins but AppliedOverrideId remains empty. Those same grouped summaries now also surface typed
selection-basis buckets plus grouped declared-versus-effective override-action buckets, so
publication-group answers can say why a host rule won and which override dimensions only stayed
declared versus materially applied without reopening the candidate catalog.
Current governance baseline:
- configure host suppression for shorthand candidates, and optionally for explicit module-DSL route
groups that both call
AllowHostGovernance()and are explicitly targeted throughAuthoringStyles = [behavior-module-dsl], throughRestApi:Suppressions - configure API-version, HTTP-method, bounded route-group-prefix, constrained
route-pattern, capability-boundary set-or-clear, explicit binding-plan replacement/merge/reset,
and endpoint-metadata set-or-clear overrides for those same governable candidates through
RestApi:Overrides - target one or more
CandidateIds, exactBehaviors, subtreeBehaviorIdPrefixes,Modules, and optionalAuthoringStyles, then optionally refine that match withApiVersionMajors,Methods,RelativePatterns,RouteGroupPrefixes,OpenApiDocumentNames,TagNames,EndpointNames,BindingFallbackModes, and exact original explicitTargetBindings - rules that omit all of
CandidateIds,Behaviors,BehaviorIdPrefixes,Modules, andHostGovernanceScopesnow still fail fast instead of suppressing every shorthand candidate implicitly, while one deliberateHostGovernanceScopesselector can now stand in as the primary authored target - override rules must define at least one override action, require a positive
ApiVersionMajorwhen that action is present, accept onlyGET,POST,PUT,PATCH, orDELETEforMethod, requirePatternto be a valid relative ASP.NET Core route pattern, requireRouteGroupPrefixto stay beneath the active REST root without placeholders or silent effective API-version changes, reject any rule that sets bothRequiredCapabilityKeyandClearRequiredCapability, and fail fast if the effective binding plan becomes invalid for the effective HTTP method - omit
AuthoringStylesto suppress both shorthand styles by default:behavior-module-profileandbehavior-module-generated - explicit module-DSL routes never enter host governance accidentally: they require both
AllowHostGovernance()on the owning route group and an explicit host rule target ofbehavior-module-dsl WithHostGovernanceScope("scope")adds one stable original-projection governance scope thatRestApi:SuppressionsandRestApi:Overridescan target without depending on rewritten route shape, but that scope alone does not opt explicit module-DSL routes into host governance- exact
CandidateIdsreuse the stable ids published byGET /engine/rest-endpoint-candidates BehaviorIdPrefixesnow target the original dot-separated behavior-id hierarchy directly, which is especially useful for grouped generated shorthand fromMapGeneratedProfileGroups(...)- that same exact-versus-prefix governance trace is now also explicitly proven through
GET /engine/rest-endpoint-publication-groups,GET /engine/rest-endpoint-authoring-policies, and the matching snapshot answers, including per-authoring-style grouped summaries where broader prefix rules can remain visible as matched- only outcomes beside narrower exact winners - those same prefix selectors now also preserve skipped-governance truth for explicit
behavior-module-dslroutes that did not opt into host governance, so grouped/operator runtime answers keep the prefix-targeted rules visible as skipped outcomes instead of collapsing them into a selector miss - the optional selector refiners and exact candidate ids all match the original shorthand
candidate shape before override actions are applied, including the original shorthand OpenAPI
document name, primary tag name, original shorthand endpoint name through
ProjectedEndpoint.OriginalEndpointName, original route-groupHostGovernanceScopethroughOriginalProjection.HostGovernanceScope, typed binding-fallback identity, and exact original explicit binding-plan identity, so governance can pick one of several shorthand candidates that share the same behavior/module identity without relying on final rewritten route shape;BindingFallbackModesuses the stable wire namespreserve-source-implicit-fallbackandpreserve-remaining-body-fallback,HostGovernanceScopesmatches the original authored scope when one exists, andTargetBindingsmatches the full original explicit descriptor set by property/source/name equivalence - overlapping behavior-targeted rules prefer exact behavior ids first and then longer
BehaviorIdPrefixes, with the winning narrower subtree reason surfaced through the stablenarrower-behavior-scopeselection basis - when more than one rule matches, Cephalon prefers candidate-targeted rules first, then fewer
targeted candidate ids, then the more specific rule by populated target dimensions, behavior-
targeted scope, narrower authoring-style scope, fewer total selector values, and finally stable
rule id ordering; shorthand candidates keep the full ordered match trace visible through
MatchedSuppressionIdsandMatchedOverrideIdsbefore one winning rule is selected - shorthand groups that already declare
.ApiVersion(...)explicitly remain authoritative over host-level version rewrites, while shorthand method and constrained pattern overrides can still apply - the current override slice rewrites only the effective API major version, HTTP method,
explicit OpenAPI document name, constrained relative route pattern, required capability
boundary, capability-boundary clear, endpoint metadata set-or-clear actions, OpenAPI tag name,
and/or explicit binding plan, including shorthand-only
ClearBindingsresets back to the implicit request-binding baseline, keeping the/v{major}route segment, OpenAPI document name, mapped endpoint, and runtime catalogs aligned;ApiVersionMajoronly re-derives the document name when the authored shorthand group did not pin one explicitly - pattern rewrites preserve the placeholder set by default and can now also rename placeholders when the effective explicit route-binding plan covers the renamed placeholder set exactly
- placeholder removals can now also apply when the original projection already exposes explicit route-binding coverage for the original placeholder set and the effective explicit binding plan keeps every affected original route-bound property explicitly bound
- placeholder additions can now also apply when the effective explicit route-binding plan covers
the full final placeholder set and every newly route-bound property was either already explicitly
bound in the original projection, for
POST/PUT/PATCHalready part of the original deterministic remaining-body fallback surface, or for shorthand candidates with no explicit binding plan already part of the original implicit query-fallback surface BindingMode = merge-explicitcan now upsert changed explicit bindings and withdraw selected original explicit bindings throughRemovedBindingProperties, while failing fast if a removal targets a property the source shorthand never bound explicitly or if one merge rule both removes and overrides the same propertyClearBindings = truecan now discard the source shorthand explicit binding plan entirely, but it fails fast if the effective route would only stay valid through explicit placeholder aliases that the clear removedTargetBindingscan now distinguish route-only shorthand candidates from richer route-plus- query/header/body profiles even when they share the same behavior/module scope and later publish under rewritten effective routesClearRequiredCapability = trueis now the explicit host answer for removing an inherited shorthand capability boundary; when that action wins, shorthand candidate projections, actual ASP.NET Core endpoint metadata,/engine/rest-endpoints, andsnapshot.RestEndpointsall keepRequiredCapabilityKey = null, andRequireCapability(...)plusClearRequiredCapability()both follow last-declaration-wins semantics so the earlier shorthand guard does not linger- ordered
RequiredFeatureFlagIdsare now the explicit host answer for rewriting an inherited shorthand REST rollout boundary, andClearRequiredFeatureFlags = trueis now the explicit host answer for removing one; when either action wins, shorthand candidate projections, actual ASP.NET Core endpoint metadata,/engine/rest-endpoints, andsnapshot.RestEndpointsall keep the same effective ordered feature ids, whileRequireFeatureFlag(...),RequireFeatureFlags(...), andClearRequiredFeatureFlags()all follow last-declaration-wins so a host can replace or remove an earlier shorthand feature gate without leaving stacked filters behind - behavior-owned feature gates can now also come from
BehaviorTopologyDescriptor.RequiredFeatureFlagIds;MapBehaviorGet(...),MapBehaviorPost(...), and the other REST helper entry points read those ids fromIBehaviorCatalog, stamp the same ordered set onto the published REST endpoint, and still leave authoritative execution gating in the shared behavior pipeline so transport-only REST rewrites do not become the only rollout truth - published endpoints now also expose nullable
OriginalRequiredCapabilityKeyplusAppliedOverrideId, so operators can read the source-versus-effective capability story directly from/engine/rest-endpointswhen shorthand governance rewrites or clears a boundary; endpoint- level capability-only no-op clears now leaveAppliedOverrideId = null - published endpoints now also expose ordered
OriginalRequiredFeatureFlagIds, so operators can read the source-versus-effective REST feature-rollout story directly from/engine/rest-endpointswhen shorthand governance rewrites or clears a boundary; endpoint-level feature-only no-op rewrites or clears now also leaveAppliedOverrideId = null - published endpoints now also expose ordered
MatchedOverrideIds, so the final runtime answer can keep shorthand override matches visible, including capability-only no-op matches, endpoint- metadata-only no-op matches, feature-only no-op matches, and reorder-only equivalent binding-plan rewrites that still leaveAppliedOverrideId = null; those no-op wins now also surface the selected winning rule throughSelectedOverrideIdplus the decisive specificity answer throughOverrideSelectionBasis; that same semantic binding-set comparison now also drives projection reuse and post-materialization structural override checks /engine/rest-endpoint-candidatesplusRuntimeIntrospectionSnapshot.RestEndpointCandidatesnow follow that same no-op governance truth for published candidates: capability-only clear matches against an already-empty boundary, same-key capability rewrites, same-value endpoint-metadata rewrites, endpoint-metadata clears against source metadata the module already removed, and reorder-only equivalent binding-plan rewrites keepMatchedOverrideIdsvisible but leaveAppliedOverrideId = null; those published candidates now also surface the winning no-op rule throughSelectedOverrideIdplusOverrideSelectionBasis, and governance-suppressed candidates now also surfaceSuppressionSelectionBasiswhen more than one suppression rule matched; in that binding-order case the source explicit binding order remains the authoritative projected answer even when the equivalent reordered binding set comes from a later synthetic or future shorthand path- broader implicit-property promotion beyond that constrained body-fallback-plus-bounded-query- fallback-plus-source-or-host-preserved-explicit-query-fallback path plus broader binding-shape overrides beyond the current replace-plus-merge-explicit upsert-plus-withdraw model remain later work
Example:
{ "RestApi": { "Suppressions": { "prefer-generated-cart": { "Behaviors": [ "showcase.cart.get" ], "AuthoringStyles": [ "behavior-module-profile" ] } } }}Override example:
{ "RestApi": { "Overrides": { "cart-v2-profile": { "Behaviors": [ "showcase.cart.get" ], "AuthoringStyles": [ "behavior-module-profile" ], "ApiVersionMajors": [ 1 ], "Methods": [ "GET" ], "RelativePatterns": [ "/{cartId}" ], "RouteGroupPrefixes": [ "/api/v1/showcase/cart" ], "ApiVersionMajor": 2, "Method": "DELETE", "RouteGroupPrefix": "/api/v2/showcase/cart-admin", "Pattern": "/lookup/{cartId}", "RemovedBindingProperties": [ "Quantity" ], "Bindings": [ { "PropertyName": "CorrelationId", "Source": "header", "Name": "X-Trace-Id" }, { "PropertyName": "Note", "Source": "body", "Name": "memo" } ], "BindingMode": "merge-explicit" } } }}For host governance config, Bindings:*:Source and TargetBindings:*:Source now use the same
stable wire-name vocabulary that runtime JSON emits: route, query, header, and body. The
behavior-authored BehaviorRestBindingSource contract now uses that same stable vocabulary for its
own JSON surface and source-generated validation flow.
Clear-bindings example:
{ "RestApi": { "Overrides": { "cart-profile-reset-bindings": { "Behaviors": [ "showcase.cart.get" ], "AuthoringStyles": [ "behavior-module-profile" ], "RelativePatterns": [ "/{cartId}" ], "ClearBindings": true } } }}That shorthand-only action removes the candidate’s explicit binding descriptors and returns request composition to the implicit route/query/body baseline. It is rejected when the final route would need placeholder aliases that only existed in the removed explicit route bindings.
The host also now fails fast when two resolved public REST endpoints collide on the same
HTTP method + route pattern.
That collision guard is distinct from behavior ownership validation:
- ownership validation rejects a module that tries to publish another module’s explicitly owned behavior
- route-collision validation rejects any duplicate resolved public REST projection, even when the conflicting endpoints came from different modules or future authoring styles
REST response envelopes
Section titled “REST response envelopes”Cephalon.Behaviors.Http now treats structured behavior outcomes and wire-format envelopes as
separate concerns:
IAppBehavior<TIn, TOut>can still return raw payload types for simple success pathsIAppBehavior<TIn, Result<TOut>>can communicate expected non-success branches such asNotFound,Invalid,Conflict,Forbidden, andNoContentwithout throwing transport-shaped exceptions- REST projects those outcomes into HTTP status codes automatically
- when
ApiRoutes:ResultEnvelope:Enabled = true, REST also wraps the payload intoResultModel<T>/ResultModelError - error envelopes use
type,status, and anerrorscollection so validation and multi-reason failures can return more than one error item cleanly while still carrying problem-details-style status metadata - the OpenAPI + Scalar response list for behavior-owned REST helpers is configurable through
OpenApi:BehaviorRest:DocumentedStatusCodes - the default documented status set is
200,201,202,204,400,401,403,404,409, and500, so server-error responses stay visible in docs by default - when ASP.NET Core rate limiting is enabled,
429is documented per endpoint when the effective rate-limiting policy actually applies to that REST route, and behavior/transport overrides can suppress it again for specific endpoints throughEngine:Resilience:RateLimiting:Overrides - when shared behavior-execution rate limiting is active, behavior-owned REST helpers also
document and return
429for behavior-dispatch rate-limit rejections - when shared behavior-execution bulkhead enforcement is active, behavior-owned REST helpers also
document and return
429for bulkhead saturation - when shared behavior-execution timeout enforcement is active, behavior-owned REST helpers also
document and return
503for timed-out dispatches - when shared behavior-execution circuit-breaker enforcement is active, behavior-owned REST helpers
also document and return
503for open-circuit rejections, including retry-after details when the runtime can compute them - if a route keeps both the ASP.NET Core endpoint limiter and the behavior-execution limiter active,
ASP.NET Core rejects first; disable the endpoint policy for that route through
Engine:Resilience:RateLimiting:Overrideswhen the behavior-owned429answer should surface instead Engine:Resilience:BehaviorExecution:Overridescan narrow or disable inherited timeout, circuit-breaker, bulkhead, and behavior-execution rate-limiting answers per behavior id, per transport id, or per behavior+transport pair withbehavior+transport > behavior > transport > defaultprecedence, and REST docs follow that resolved runtime answer per endpoint- GraphQL and JSON-RPC keep their protocol-native response shapes and are intentionally not wrapped
in
ResultModel - when behavior-owned execution rate limiting rejects a generic behavior HTTP request, GraphQL HTTP,
JSON-RPC, GraphQL-SSE, GraphQL-WS, SSE, and WebSocket bindings keep their native error envelopes
while surfacing the shared Cephalon limiter code plus
429metadata instead of flattening the rejection into a generic transport failure - when behavior-owned execution timeout rejects a generic behavior HTTP request, GraphQL HTTP,
JSON-RPC, GraphQL-SSE, GraphQL-WS, SSE, and WebSocket bindings keep their native error envelopes
while surfacing the shared
behavior_execution_timeoutcode plus503metadata instead of flattening the rejection into a generic transport failure - when behavior-owned execution circuit-breaker rejections hit a generic behavior HTTP request,
GraphQL HTTP, JSON-RPC, GraphQL-SSE, GraphQL-WS, SSE, and WebSocket bindings keep their native
error envelopes while surfacing the shared
behavior_execution_circuit_breaker_opencode plus503metadata and retry-after timing when the runtime can compute it - JSON-RPC keeps those
503-class semantics in-band through a dedicated-32053service-unavailable server error whileerror.datapreserves the stable Cephalon code, message, and optional retry-after details
Example host override:
{ "OpenApi": { "BehaviorRest": { "DocumentedStatusCodes": [200, 400, 404, 500] } }}Example:
public sealed class GetCartBehavior : IAppBehavior<GetCartInput, Result<GetCartOutput>>{ public async Task<Result<GetCartOutput>> HandleAsync( GetCartInput input, IBehaviorContext context, CancellationToken cancellationToken = default) { var cart = await LoadCartAsync(input.CartId, cancellationToken); if (cart is null) { return Result.NotFound( "cart.not_found", $"Cart '{input.CartId}' was not found."); }
return Result.Ok( new GetCartOutput(cart), message: "Cart resolved."); }}With ApiRoutes:ResultEnvelope:Enabled = true, REST projects that contract into payloads such as:
{ "title": "Ok", "message": "Cart resolved.", "success": true, "status": 200, "data": { "cartId": "cart-123" }}and:
{ "type": "https://tools.ietf.org/html/rfc9110#section-15.5.5", "title": "Not found", "message": "Cart 'cart-123' was not found.", "success": false, "status": 404, "data": null, "errors": [ { "key": "cart.not_found", "message": "Cart 'cart-123' was not found.", "severity": "error", "details": null } ]}Multi-reason validation faults project cleanly too. The showcase AddToCartBehavior now returns
Result.Invalid(...) with nested BehaviorFault.InnerFaults, which REST
projects to payloads such as:
{ "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1", "title": "Invalid request", "message": "Cart add-item request is invalid.", "success": false, "status": 400, "data": null, "errors": [ { "key": "showcase.cart.add_item.product_id.required", "message": "Product id is required.", "severity": "error", "details": null }, { "key": "showcase.cart.add_item.quantity.invalid", "message": "Quantity must be greater than zero.", "severity": "error", "details": null } ]}Keep that envelope as a REST host policy only. Messaging, events, GraphQL, and JSON-RPC should not reuse it as a universal engine contract.
Feature-gated behavior execution
Section titled “Feature-gated behavior execution”- module-owned REST helper endpoints now project behavior-authored
BehaviorTopologyDescriptor.RequiredFeatureFlagIdsinto the published endpoint shape so/engine/rest-endpointsand request execution keep the same ordered gate list when the behavior catalog declares a rollout boundary - when shared behavior execution rejects a REST request with
BehaviorFeatureDisabledException, the helper returns404 ProblemDetailsby default, orResultModelErrorwhen the REST result envelope is enabled, withbehaviorId, the decisivefeatureFlagId, the full orderedrequiredFeatureFlagIds, and source ownership metadata instead of pretending the endpoint disappeared from runtime truth JsonRpcHttpBehaviorBindingkeeps that same behavior-owned gate protocol-native through JSON-RPC server error-32004with messageFeature not available
DefaultBehaviorContext metadata conventions
Section titled “DefaultBehaviorContext metadata conventions”| Source | Maps to |
|---|---|
X-Correlation-Id | Metadata["CorrelationId"] |
X-Tenant-Id | Metadata["TenantId"] |
Authorization (sub claim) | Metadata["UserId"] and Metadata["SubjectId"] |
IHostEnvironment.EnvironmentName | Metadata["EnvironmentName"] |
| Transport binding | Metadata["TransportId"] |
X-Meta-* | Metadata[key-without-prefix] |
Those ambient values are also what the shared behavior feature-gate middleware uses when it builds
the evaluation context for IFeatureToggle.
Status: ✅ Shipped — runtime ownership metadata covered by focused hosting and package-surface validation
Related components
Section titled “Related components”Cephalon.Behaviors— dispatcher, catalog, resolver, compatibility rules (required dependency)Cephalon.Abstractions— behavior contractsCephalon.AspNetCore— host-level OpenAPI + Scalar surface for REST endpoints