Brainograph.ServiceDefaults
4.6.0
dotnet add package Brainograph.ServiceDefaults --version 4.6.0
NuGet\Install-Package Brainograph.ServiceDefaults -Version 4.6.0
<PackageReference Include="Brainograph.ServiceDefaults" Version="4.6.0" />
<PackageVersion Include="Brainograph.ServiceDefaults" Version="4.6.0" />
<PackageReference Include="Brainograph.ServiceDefaults" />
paket add Brainograph.ServiceDefaults --version 4.6.0
#r "nuget: Brainograph.ServiceDefaults, 4.6.0"
#:package Brainograph.ServiceDefaults@4.6.0
#addin nuget:?package=Brainograph.ServiceDefaults&version=4.6.0
#tool nuget:?package=Brainograph.ServiceDefaults&version=4.6.0
Brainograph host bootstrapping for new bg-* services: AddBrainographServiceDefaults / MapBrainographDefaults (Serilog, OpenTelemetry, health probes, auth, rate limiting, exception-to-envelope pipeline, platform JSON wire) plus PlatformControllerBase.
| 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
- Brainograph.Utilities (>= 6.28.0)
- Microsoft.AspNetCore.Authentication.JwtBearer (>= 10.0.5)
- Microsoft.AspNetCore.Mvc.NewtonsoftJson (>= 10.0.5)
- Microsoft.AspNetCore.OpenApi (>= 10.0.5)
- Microsoft.EntityFrameworkCore (>= 10.0.5)
- Microsoft.IdentityModel.Protocols.OpenIdConnect (>= 8.16.0)
- Microsoft.IdentityModel.Tokens (>= 8.16.0)
- Microsoft.OpenApi (>= 2.7.5)
- Newtonsoft.Json (>= 13.0.4)
- OpenTelemetry.Exporter.Console (>= 1.16.0)
- OpenTelemetry.Exporter.OpenTelemetryProtocol (>= 1.16.0)
- OpenTelemetry.Extensions.Hosting (>= 1.16.0)
- OpenTelemetry.Instrumentation.AspNetCore (>= 1.16.0)
- OpenTelemetry.Instrumentation.Http (>= 1.16.0)
- Serilog.AspNetCore (>= 10.0.0)
- Serilog.Formatting.Compact (>= 3.0.0)
- System.IdentityModel.Tokens.Jwt (>= 8.16.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|
4.6.0 — logging defaults (owner 2026-09-19, «удали все лишние логи»; dev 1's log-noise census), applied by the host as a Serilog Filter.ByExcluding after the level policy and configuration. No rule raises a level and none ever drops Warning or above, because the libraries concerned log some FAILURES at Information; the discriminator is always an event id or an exact message template. (1) IHttpClientFactory's request start and end - event ids 100 and 101 under System.Net.Http.HttpClient.*, in both handlers - are dropped by default: the largest source (one identity client alone: 588 lines / 498 KB a day). Id 104, "HTTP request failed", stays: Microsoft.Extensions.Http (decompiled, identical at the 10.0.5 pin and in the 10.0.12 shared framework) logs it at Information with the exception and has no Warning or Error event, so a level raise would have hidden every outbound failure. THE COST, stated: the End line was the only place the pipeline logged a non-success STATUS, so a 4xx/5xx answer is no longer logged by the pipeline and client code logs its own refusals. Observability:LogExclusions:DropHttpClientRequestInformation=false restores the lines. (2) Observability:LogExclusions:Events - a service drops events by (SourceContext namespace, event id), for success chatter that shares a category and a level with its signal: OpenIddict logs token-request successes AND rejections at Information in one category (OpenIddict.Server 7.5.0: 159 of 191 Information calls precede a Reject), so only the id separates them. The prefix is a namespace: OpenIddict.Server matches OpenIddict.Server.X and never OpenIddict.ServerX. Array form, so no environment variable name needs a dot. (3) Wolverine's lifecycle lines are dropped by default: the exact 15 message templates (16 category pairs) that were all 378 id-less Wolverine lines in 7 days of usercontent (dev 3), each a constant template in WolverineFx / WolverineFx.RabbitMQ 6.21.0 (decompiled). NOT "every id-less line": WolverineFx also logs dead-lettering, permanent latching, back-pressure, pauses and requeues at Information with no id, and those keep logging, as does every event WITH an id (106 no handler, 107 no routes, 108 discarding, 204-210 transport state). A template Wolverine adds or rewords keeps logging until it is measured and listed. Observability:LogExclusions:DropWolverineLifecycleInformation=false turns it off. A BLANK value in the section means unset (the 4.1.0 rule, so clearing a setting cannot crash-loop the host); a malformed one fails startup with a message naming the section. API: new LogEventExclusions / LogEventExclusionOptions / LogEventExclusionRule (additive). WHAT AN ADOPTER ALSO GETS from the versions it skips: 4.2.0 authorization-denial auditing and 4.3.0 an honest /health/ready (both opt-in, inert unless used); 4.4.0 the ObsoleteOperationTransformer (OpenAPI); 4.5.0 the forwarded-headers pass FAILS CLOSED - a host with it enabled and no trusted peer refuses to start. DEPENDENCY FLOOR, named: the packed nuspec depends on Brainograph.Utilities at the sibling csproj's version at pack time, 6.28.0 (4.5.0 declared 6.26.0; every other dependency is unchanged, measured by diffing the two nuspecs). A service adopting 4.6.0 moves its Utilities pin to 6.28.0 first, as its own named change. From below 6.27.0 that move carries 6.27.0's [HasPermission] change (a service principal holding the scope an action's [AllowServiceScope] declares passes that action's user gate), so it ships alone and named. 4.5.0 — The forwarded-headers pass FAILS CLOSED: with it enabled and no trusted peer, the host
refuses to start.
BREAKING ON RE-PIN FOR A MISCONFIGURED SERVICE, inert for every other. Not an API change — nothing
is removed and no signature moves — but a host that used to start may now refuse to.
WHAT WAS WRONG. AddBrainographServiceDefaults clears KnownIPNetworks and KnownProxies when
ForwardedHeaders:Enabled is true, and restores neither; the in-cluster reasoning is that only the
ingress can reach the pod. But the framework middleware runs its peer check ONLY when one of the two
lists is non-empty — measured in Microsoft.AspNetCore.HttpOverrides 10.0.12, where
`KnownIPNetworks.Count > 0 || KnownProxies.Count > 0` gates the CheckKnownAddress branch. So an empty
PAIR is not "trust nobody", it is no check at all, and every caller's X-Forwarded-For is believed.
That header is what per-IP rate-limit partitions and access logs are written from.
THE REFUSAL is a startup validator over ForwardedHeadersOptions, registered only when the pass is
enabled and armed with ValidateOnStart. It fails when, after every Configure delegate has run, both
trust lists are empty. The message names the three ways out by their exact spelling —
ForwardedHeaders:KnownIPNetworks, ForwardedHeaders:KnownProxies, ForwardedHeaders:TrustAllPeers —
because whoever meets it in a test suite would otherwise have to read the kernel to continue. It
surfaces as OptionsValidationException from StartAsync.
WHY THE RESOLVED OPTIONS AND NOT THE CONFIGURATION KEYS. bg-identity and bg-gateway re-add their
network IN CODE with no configuration key at all, so a check over keys would refuse exactly the two
services that are correct. Nor can the check live inside the kernel's own Configure delegate: that
delegate runs FIRST, so at that moment the lists are empty for everyone. A validator sees the final
state by construction — which is also why it still fires for a host that composes its own pipeline
and never calls MapBrainographDefaults.
THE DECLARED WAY OUT is ForwardedHeaders:TrustAllPeers=true. The validator then passes and logs one
Warning per start, EventId 4260, naming the key. It exists so that the answer to a refusal is never
"turn the pass off", which would silently drop X-Forwarded-Proto as well and is invisible in a diff
of one boolean. NO CURRENT CONSUMER NEEDS IT; it ships unused, on purpose.
FORWARDLIMIT IS UNTOUCHED, deliberately. The framework default is 1 and the kernel never sets it, so
a service that re-adds trust and leaves the default gets a SINGLE-HOP walk — correct behind one
proxy, wrong behind two. bg-identity and bg-gateway set it to null and let trust bound the walk,
because the number of hops is Azure's deployment detail and any constant is right until an
infrastructure change adds one and it fails silently. The guard neither requires a limit nor warns
when it is null; a fact pins that null is accepted.
WHO THIS AFFECTS ON RE-PIN, from each service's committed configuration:
bg-identity pass on in appsettings.Production.json; re-adds 100.100.0.0/16 in code and sets
ForwardLimit = null. STARTS. Adopt FIRST: it owns ForwardedHeadersLayeringTests,
so adopting there proves the guard leaves a correct configuration alone.
brainograph-kg pass on in BOTH appsettings.Production.json and appsettings.Staging.json, and
nothing in its src mentioned either list — the one service this guard would have
stopped. ITS FIX IS ALREADY MERGED, at main 3a602a4: it re-adds the overlay as a
network after the kernel's clear, pinned by four facts on the real host. SO NO
CONSUMER FAILS THIS GUARD TODAY, and adoption is a pin move rather than a repair.
bg-gateway NOT A CONSUMER. It references no Brainograph.ServiceDefaults and no
Brainograph.Utilities at all; its only Brainograph package is Analytics.Contracts.
It configures ForwardedHeadersOptions and calls UseForwardedHeaders itself, and it
does clear then re-add 100.100.0.0/16 with ForwardLimit = null — so it is correct
today FOR ITS OWN REASONS, NOT THE KERNEL'S. There is nothing for it to re-pin and
this guard can never fire for it.
bg-content, the kernel's pass is OFF for both; they call UseForwardedHeaders themselves with
bg-media their own options object and are handed the network through configuration.
Unaffected. bg-media already implements this same predicate for itself
(ForwardedTrust.StartupPermitted) and can later reduce it to the kernel's, as its
own named change rather than part of this one.
analytic, dictionary, usercontent, notification, aiagent, feedback, gis — enable nothing, so there
is no middleware and an empty trust list is not a misconfiguration. A fact pins
that the guard stays silent with the pass off.
WHAT AN ADOPTER MUST SATISFY is exactly one thing: with ForwardedHeaders:Enabled true, at least one
entry in KnownIPNetworks or KnownProxies once its own Configure delegate has run — or TrustAllPeers
declared.
MEASURED 2026-09-17, SDK 10.0.401, container-free: red 7 discovered / 7 executed with the 2
registered failures; green 8/8 on the guard's facts and 196/196 on the whole
Brainograph.ServiceDefaults.Tests project, no Aborted line; six pre-registered mutations all killed
with exactly the predicted failure sets. One mutation — dropping ValidateOnStart — SURVIVED the
first round, and that is how the fact for a host which never calls MapBrainographDefaults came to
exist: without it, the registration that makes the guard general was untested.
WHAT 4.5.0 CARRIES, IN TWO MEASUREMENTS THAT DO NOT AGREE — read both. The only change to
src/Brainograph.ServiceDefaults since 4.4.0's version bump (5fd20d1, 2026-08-28) is this one commit.
But the PACKAGE moves the kernel floor: 4.4.0 declared Brainograph.Utilities >= 6.20.0 and 4.5.0
declares >= 6.26.0, and Microsoft.IdentityModel.Protocols.OpenIdConnect 8.16.0 appears as a new
transitive dependency. That is not a consequence of this commit — the nuspec dependency rides the
sibling project's version, so packing stamps whatever Utilities is at in the tree. The floor is left
at 6.26.0 on purpose: it is what this assembly was compiled against, and a ServiceDefaults floor has
been raised for exactly this reason before (see 3.2.0's clause below — restoring an older Utilities
silently fails to deliver its buildTransitive props and XML docs even when the code on disk is the
same). Declaring a lower one would assert a compatibility nobody has measured.
SO, PLAINLY: a consumer already on Utilities 6.26.0 takes one pin move. A consumer below it takes
TWO kernel moves in one restore, the second of which brings the service-token release and everything
else between its pin and 6.26.0. Measured on 2026-09-17, brainograph-kg is on 6.26.0 and bg-identity
on 6.25.0; the rest of the fleet sits between 6.11.0 and 6.23.0. Plan the adoption as a kernel pin
move, named, not as a patch bump.
AND NOTHING ENFORCES THE SECOND MOVE — measured, because the obvious assumption is wrong in the
dangerous direction. Every service in this fleet references Brainograph.Utilities with its own
PackageReference as well as taking it through this package. With such a direct, centrally managed
pin present, NuGet resolves the pinned version and SAYS NOTHING: restoring ServiceDefaults 4.5.0
beside a pinned Utilities 6.25.0 exits 0, with no NU1605 and no NU1109, and the assets file records
4.5.0 sitting on 6.25.0. (Remove the direct reference and the same restore fails with NU1109 — but
no service is shaped that way.)
So a service CAN pin this package alone, restore green, build green, and run an assembly compiled
against 6.26.0 on top of 6.25.0 — a pairing nobody has built or tested. The failure that would
follow is a MissingMethodException at the moment the affected path first runs: not at restore, not
at startup, and nowhere near anything that names the pin. MOVE BOTH PINS IN THE SAME COMMIT. It is
a discipline, not a gate, and it is the one thing about this release an adopter cannot discover by
trying it.
PREVIOUSLY, 4.4.0 — no note was written into this field at the time. Reconstructed from git, it
carried a single commit, 39107aa: ObsoleteOperationTransformer, the shared OpenAPI transformer that
makes [Obsolete] visible on a generated operation.
PREVIOUSLY, 4.3.0 — An honest /health/ready (ADDITIVE, opt-in, inert unless used).
/health/ready was a constant: Results.Ok(new { status = "ready" }). It checked nothing — not the
schema, not the broker, not the consumer hosts — so it was a second liveness probe under a
misleading name, the exact defect INVARIANTS.md 5 names ("a check must answer the question it
appears to answer"). It is now a real health endpoint filtered to the "ready" tag.
NOTHING CHANGES FOR A SERVICE THAT DOES NOT OPT IN. With zero registered checks the aggregate is
Healthy and the endpoint emits the byte-identical {"status":"ready"} it always did, still anonymous
and still ExcludeFromDescription. /health/live remains a constant and can never be answered by a
readiness check — the tag filter enforces the split rather than discipline.
Opt in with services.AddBrainographReadiness(), then:
.AddStartupGate(gate) — a latching tri-state; the ONLY thing that can answer 503.
.AddDegradedComponent(state) — up/degraded; its worst answer is HTTP 200 + "degraded".
Degraded maps to 200 BY CONSTRUCTION, not by convention. Every backend runs at minReplicas:1 in
Single revision mode, so a 503 does not shift traffic to a sibling — it takes the service to zero.
A stalled consumer or an unreachable broker must therefore never de-rotate a replica; it is reported
in the body, where tooling and humans read it, while the service keeps serving. Making that a TYPE
(ComponentState cannot return Unhealthy) means no future service can wire "broker down" into
out-of-rotation by accident.
StartupGate is LATCHING on purpose: once it has seen success, MarkFailed is a no-op. It answers a
question about this process's lifetime ("was the schema this binary needs ever observed current?"),
which cannot become false again — so a thirty-second database blip cannot redden a serving replica.
Before the first success it accepts either outcome, so a migrator that retries with backoff moves
Pending -> Failed -> Succeeded without a restart. Wiring a gate to a migrator that gives up after
one attempt would strand the replica red forever: retry is the caller's obligation.
The body carries component NAMES only. It is reachable through the public gateway facade, and a
migration failure's real message carries host names, user names and connection details; reasons stay
on the gate for logs.
PREVIOUSLY, 4.2.0 — Authorization-denial auditing (ADDITIVE, opt-in, inert unless used).
Adds Brainograph.ServiceDefaults.Authorization: an AuthorizationDenial record, an
IAuthorizationDenialSink seam, and middleware that observes a 403 and hands a description to
whichever sink the service registered. The kernel owns the MECHANISM; each service owns its audit
SCHEMA, because the services with audit tables have deliberately different shapes.
Enable with app.UseAuthorizationDenialAuditing() — and call it BEFORE MapBrainographDefaults(),
which invokes UseAuthentication/UseAuthorization itself. Registered after it instead, the middleware
sits INSIDE authorization, where AuthorizationMiddleware short-circuits a policy failure without
calling next, so every class-level [Authorize(Roles=...)] refusal is silently invisible.
Deliberately NOT wired into MapBrainographDefaults(): denied attempts are the first audit rows whose
rate someone other than an administrator chooses, so a fleet-wide kernel re-pin must never start
writing them in a service whose retention sweep has not shipped. With no sink registered the
middleware does nothing at all — one null service lookup per 403 — which is what makes this safe for
the services that have no audit table.
Records numeric permission ordinals (a frozen wire format; also, seven permission names exceed the
32-character cap a consuming service applies to audit tokens), deduplicated and ordered. The sink is
given CancellationToken.None, never the request's token: this runs after the 403, so the request's
token would let the audited party cancel the write recording their own probe. Denials are throttled
to one per (actor, endpoint) per five minutes, per replica.
PREVIOUSLY, 3.2.0 — Self-describing OpenAPI. Controller actions returning IActionResult now document their real
response schema: a source generator (shipped as an analyzer asset in this package) recovers the
payload type from each action's own Ok1/OkMany call, and an application-model convention attaches
ProducesResponseType, so schema generation, XML doc comments and enums are all stock ASP.NET from
there. Enums document as string names (previously integers, which contradicted the wire); documents
are pinned to OpenAPI 3.0; responses advertise application/json only; each service declares its own
BearerAuth scheme; and actions that can call Fail(...) document the HTTP-200 error union. Also
excludes /health/live and /health/ready from the OpenAPI document (ExcludeFromDescription) — they
are infrastructure probes, not API surface, and the gateway's catalog merger already assumed they
were absent; expect them to disappear from a consuming service's document on upgrade. No wire
changes — response bytes are unchanged and pinned by EnvelopeWireOverHttpTests. Consuming services
must set GenerateDocumentationFile=true to get endpoint descriptions, and should add
<NoWarn>;CA1014;CA2007;1591</NoWarn> alongside it for their OWN undocumented public members —
that covers CS1591 only; this package's buildTransitive targets already demote the OTHER
XML-documentation diagnostics an 8-service, never-before-XML-documented codebase will actually hit
the moment doc generation turns on (CS1570/1572/1573/1574/1584/1734 — e.g. CS1734 from a
<paramref> naming a parameter that doesn't exist, a hard error under TreatWarningsAsErrors), so
a service does not need to hunt those down itself. Also need a DIRECT PackageReference to
Microsoft.AspNetCore.OpenApi (its interceptor wiring ships in build/, not
buildTransitive/, so it is not inherited). Depends on Brainograph.Utilities 6.3.0, not just 6.2.0 —
6.3.0 is the first Utilities version whose own envelope XML doc and buildTransitive props actually
reach a consumer from the feed (see Brainograph.Utilities' 6.3.0 PackageReleaseNotes); restoring
Utilities 6.2.0 silently gets neither, even though its code carries the same props/XML on disk.
PREVIOUSLY, 3.1.0 — No API change; re-versioned because the package now depends on Brainograph.Utilities 6.2.0, which adds PermissionClaimCodec and the compact-permission-claim expansion shim in JwtAuthentication.OnTokenValidated. Bumped rather than repacking 3.0.0 in place: NuGet caches by (id, version), so mutating a published version leaves already-restored machines on stale content. Consumers get the shim — i.e. the ABILITY to read compact tokens — by moving to this version; nothing emits the new format until bg-identity is changed separately. PREVIOUSLY, 3.0.0 — Unified Users: CurrentUserId() now returns Guid (was long; same claim fallback + throw behavior); adds CurrentLegacyUserId() → long? from the additive legacy_sid claim. Depends on Brainograph.Utilities 6.0.0.