Default guidance for changes in this repository. These are conventions and guardrails, not immutable law.
Package inventories, registration recipes, and feature-specific detail live in site/content/docs/architecture/README.md, site/content/docs/architecture/dependency-graph.md, and site/content/docs/architecture/hosted-services.md.
- Treat every section as a default, not a veto. When a task conflicts with a rule here or in
site/content/docs/, say so plainly: what the rule expects, what the task needs, and the trade-off. - Propose alternatives. Offer at least one viable path that follows the guide and, when useful, one that bends or breaks it with justification. Counter-argue your own recommendation when the trade-offs are close.
- Override only with confirmation. If the user accepts a deviation (forbidden role edge, skipped docs, different package shape, and similar), proceed and note the exception in the change summary. Do not silently ignore a rule.
- Suggest guide updates. When repeated overrides, new patterns, or outdated docs show a rule no longer fits, recommend a concrete edit to
AGENTS.mdor the relevantsite/content/docs/file. The user decides whether to adopt it. - Prefer dialogue over deadlock. A short question beats a long assumption. If confirmation is unclear, ask once with options rather than blocking on rigid compliance.
Write for experienced .NET developers who already understand messaging, CQS, DDD, and dependency injection.
- Use plain ASCII punctuation. Do not use em dashes, smart quotes, emoji, or decorative symbols.
- Lead with the exact capability, then a runnable example, then constraints and explanation. Keep reference pages dense and precise.
- Describe LiteBus in technical terms: command, query, and event mediation, with optional inbox, outbox, saga, storage, transport, hosting, and observability modules.
- Prefer nouns and verbs over slogans or taglines. Headings name the capability. Use
Durable Inbox and Transactional Outbox; avoid claims such asMessages That Never Get Lost. - Do not use sales cadence, rhetorical fragments, or unsupported superlatives. State the API, execution model, durability boundary, or integration directly.
- Prefer
command, query, and event mediationoverin-process mediatorunless process locality is the subject. When locality matters, say that handlers execute in the caller's process. - Describe durable behavior with exact terms such as persisted inbox acceptance, transactional outbox enqueueing, lease-based processing, and the configured storage or transport adapter.
- Use CQS and DDD terminology where it clarifies API intent. Do not present those terms as branding.
- When comparing LiteBus with MediatR, lead with the licensing difference and the semantic command, query, and event model. Keep
IRequestreplacement details in migration documentation, not landing-page copy. Verify and cite current external licensing claims. - State the LiteBus license policy directly: LiteBus is MIT licensed, open source, free for commercial use, and will remain free. Do not turn the policy into a tagline.
- Use Title Case for consumer-facing page headings, section headings, navigation labels, and primary buttons. Use sentence case for body text.
- Use exact, sourced numbers. Label illustrative examples as illustrative and do not present them as benchmarks.
- State meaningful limitations and opt-in boundaries alongside the related capability. For example, durable processing requires a selected store and processor host; mediation alone does not persist messages.
All C# under src/ must use XML documentation comments (///) on every construct, including private and internal members. This applies to shipping libraries consumers reference and to internal implementation details agents maintain.
| Construct | Required tags |
|---|---|
| Namespace | Not required (no XML on namespace declarations) |
Type (class, struct, record, interface, enum, delegate) |
<summary>; <remarks> when behavior is non-obvious |
| Public / internal / protected members | <summary>; <param> per parameter; <returns> when not void; <typeparam> per type parameter |
| Private members | <summary> at minimum; <param> / <returns> / <typeparam> when applicable |
| Private and internal fields | <summary> describing role and lifetime |
| Explicit interface implementations | <inheritdoc /> or explicit <summary> |
| Constructors | <summary>; <param> for each parameter |
| Properties | <summary>; <value> when the meaning of the value is not obvious from the name |
- Indent summary text with four spaces after the opening tag (same as current public API docs).
- Use
<see cref="TypeName" />for references to types and members in this solution. - Use
<see langword="null" />,<see langword="true" />, and<see langword="false" />where appropriate. - Prefer complete sentences in summaries.
- Do not document auto-generated designer or assembly attribute boilerplate.
- Replace member-level
//comments with///when documenting that member; keep//only for local algorithm notes inside method bodies.
/// <summary>
/// Registers a message type with a stable contract name and version.
/// </summary>
/// <typeparam name="TMessage">The concrete message type to register.</typeparam>
/// <param name="name">The stable contract name stored in persisted envelopes.</param>
/// <param name="version">The positive contract version stored with the payload.</param>
/// <returns>The registry so module builders can chain registrations.</returns>
IMessageContractRegistry Register<TMessage>(string name, int version = 1)
where TMessage : notnull;
/// <summary>
/// Gets the message registry used to register handlers and message types.
/// </summary>
private readonly IMessageRegistry _messageRegistry;/// <summary>
/// Links newly discovered handler descriptors to committed message descriptors.
/// </summary>
/// <param name="newDescriptors">The handler descriptors to link.</param>
private void LinkHandlersToCommittedMessages(IList<IHandlerDescriptor> newDescriptors)benchmarks/are not required to follow documentation rules unless a task explicitly says otherwise.- Do not add XML comments that restate the identifier without adding meaning (for example,
/// <summary>Gets the count.</summary>onCountis acceptable;/// <summary>Count.</summary>is not). - Do not add file header blocks (
// <copyright>, license banners). LiteBus uses per-member///documentation only.
tests/andsamples/: require XML documentation on public types and members in test helpers, shared fixtures, and sample host entry points. Private test methods and nested types do not require///unless a task says otherwise.- StyleCop documentation warnings apply via
tests/.editorconfigandsamples/.editorconfig.
After editing src/:
dotnet build LiteBus.slnxStyleCop.Analyzers is referenced from src/Directory.Build.props. Fix documentation analyzer warnings (SA1600 through SA1629) before finishing a documentation task. File header rules (SA1633 and related) are disabled. See Code style and analyzers for the full analyzer inventory on src/, tests/, and samples/.
Conventions below apply during cleanup and to all new code. Analyzer severities live in src/.editorconfig, tests/.editorconfig, samples/.editorconfig, and ReSharper inspection keys in the root .editorconfig. src/Directory.Build.props enables AnalysisLevel, AnalysisMode, and EnforceCodeStyleInBuild.
- Always use static throw helpers:
ArgumentNullException.ThrowIfNull(...)ArgumentException.ThrowIfNullOrWhiteSpace(...)/ThrowIfNullOrEmpty(...)where applicableArgumentOutOfRangeException.ThrowIfLessThan/ThrowIfNegativefor range checks
- Reserve
throw new ArgumentNullException(nameof(x))only when a custom message is required.
- One top-level type per file (class, interface, record, struct, enum, delegate).
- Allowed exceptions (document explicitly):
private/internalnested types inside apartialclass (for example HTTP JSON DTOs inLiteBusManagementEndpointModels.cs).
- Generic filenames: bracket arity, as in
ICommandHandler[TCommand, TCommandResult].cs. Do not use CLR backtick names. - Non-generic and generic variants are separate files (split pairs like
InboxAcceptItem+InboxAcceptItem<TMessage>).
- File-scoped namespaces only:
namespace Foo.Bar; - Block-scoped
namespace Foo.Bar { ... }is not allowed (IDE0161is error onsrc/, warning ontests/andsamples/).
- Do not use C# 12 primary constructors on
classtypes. They clutter the type header and hurt readability in a public library. - Use explicit fields/properties plus a conventional constructor body (existing LiteBus pattern).
- Positional records for immutable DTOs/value objects remain fine (
public sealed record Foo(string Bar);). - Do not introduce new primary-constructor classes during cleanup; convert any found to explicit constructors.
IDE0290is disabled so Roslyn does not suggest primary constructors.
- Records and immutable DTOs:
{ get; init; }or positional records. - Documented exceptions where
setstays:- EF Core entity / projection types.
- Intentionally mutable pipeline state (for example
MessageErrorContext). - ASP.NET two-way binding types (rare).
- Use collection expressions
[],[x],[..items]instead of:Array.Empty<T>()new List<T>()/new T[] { }when building inline collectionsEnumerable.Empty<T>()for empty returns
- Apply on every touched file during cleanup; grep for legacy patterns and convert.
- Prefer
paramsread-only spans/arrays for variadic convenience APIs where callers pass a small, inline list:- Module registration helpers (
RegisterDiagnosticCheck, tag lists, module type lists). - Internal builder wiring with 2-5 homogeneous arguments.
- Module registration helpers (
- Keep
IReadOnlyList<T>for batch/domain writer APIs per existing API Design rules (AcceptBatchAsync,EnqueueAsyncbatch entries, parallel business data). Do not replace batch contracts withparams. - When converting, use
params ReadOnlySpan<T>orparams T[]and wrap toIReadOnlyListinternally only at the public batch boundary if needed.
[Flags]only when values are combined with|,&, or~.- No bitwise ops on plain enums (
CA1069).
- All
awaitin library code (src/, and tests/samples where async) must use.ConfigureAwait(false)unless documented host-context requirement (ASP.NET host-adapter edge only) (CA2007).
- In scope: rewrite
catch (Exception)at transport ingress, publishers, consumers, and dispatch adapters. - Replace with specific catches where the SDK documents them (
OperationCanceledException, broker-specific exceptions,JsonException, and similar). - When a broad catch is unavoidable at a boundary, catch
Exception, log with context, map to a domain transport/dispatch exception, and never swallow silently. - Add XML
<remarks>on the catch block explaining why the boundary remains broad if specificity is impossible. CA1031warns on generalcatch (Exception); suppress only with justification.
Add where missing on public surfaces touched during cleanup:
| Attribute | When |
|---|---|
[EnumeratorCancellation] |
CancellationToken parameter on IAsyncEnumerable / async-iterator methods |
[DebuggerDisplay] |
High-churn public value types (envelopes, receipts, key options), with concise output and no PII |
[DynamicallyAccessedMembers] / [RequiresUnreferencedCode] |
Reflection paths (MessageRegistry, contract resolution, saga state discovery) |
[EditorBrowsable(Never)] |
Advanced/internal registration hooks exposed as public for framework reasons |
[Obsolete(message, error: false)] |
Already used for telemetry renames; keep pattern consistent |
Do not add attributes that duplicate compiler/NRT behavior or bloat every type.
| Rule | ID | Severity | Purpose |
|---|---|---|---|
Use ThrowIfNull |
CA1510 | warning | Null argument validation |
Use ThrowIfNullOrEmpty |
CA1511 | warning | String/collection null-or-empty |
| ConfigureAwait | CA2007 | warning | Library async discipline |
| Non-Flags enum bitwise | CA1069 | warning | Enum misuse |
| One type per file | SA1402 | warning | File layout |
| File name matches type | SA1649 | warning | Generic [T] naming |
| File-scoped namespace | IDE0161 | error | Block-scoped namespaces forbidden |
| Collection expression | IDE0300 | warning | Prefer [] syntax |
| Primary constructor on class | IDE0290 | none | Explicitly disabled |
| Catch general Exception | CA1031 | warning | Transport/boundary review |
Keep disabled: StyleCop layout/spacing categories, SA1633 file headers, IDE0007/var wars, CA1062 (redundant with NRT + CA1510).
Root .editorconfig sets resharper_*_highlighting = warning for redundant usings, file-scoped namespaces, ConfigureAwait, collection/array style, redundant defaults, casts, using declarations, this. qualification, always-true/false conditions, and unused private members. Do not enable inspections that recommend primary constructors on classes or bulk LINQ simplifications that hurt hot-path readability.
After editing a batch, call ReadLints on touched paths and fix ReSharper warnings at warning severity.
Every package belongs to exactly one dependency role. A package may reference only the project and package roles allowed by the matrix below.
Forbidden role edges are the default failure mode in review. Before adding a project or package reference, confirm that the target is allowed for the source role. If a feature needs a forbidden type, the usual fix is to move the contract into an allowed contract role, introduce an abstraction, or add a dedicated feature or host adapter. If none of those work, explain why and get explicit approval before merging a violation.
| Role | Purpose | Allowed dependencies |
|---|---|---|
| Platform contracts | Cross-cutting runtime and transport abstractions | Platform contracts and the BCL |
| Mediation contracts | Messaging and semantic mediator contracts | Platform and mediation contracts, plus the BCL |
| Durable contracts | Durable messaging and saga contracts | Platform, mediation, and durable contracts, plus the BCL |
| Core implementation | Default implementations without technology or host coupling | Contract roles and other core implementations |
| Technology adapter | One persistence or broker technology | Contract roles, core implementations, technology adapters, and one relevant SDK family |
| Feature bridge | Storage, dispatch, ingress, or cross-feature integration | Contract roles, core implementations, technology adapters, and relevant feature bridges |
| Host adapter | DI, hosting, OpenTelemetry, health checks, or ASP.NET Core | Applicable roles and the relevant host framework |
| Consumer tooling | Analyzers and test support | Roles and packages required by the tool |
| Aggregate | The LiteBus convenience package |
Contract and core implementation roles only |
The current package-to-role map and exact allowlist are maintained in Dependency Graph and enforced for every src/**/*.csproj by ArchitectureDependencyPolicyTests.
| Suffix / pattern | Role | Typical dependency role |
|---|---|---|
*.Abstractions |
Contracts only; no concrete SDK or hosting references | Platform, mediation, or durable contracts |
| Core package (no suffix) | Default implementation for one domain concern | Core implementation |
*.Storage.* |
Persistence adapter | Technology adapter or feature bridge |
*.Dispatch.* / *.Ingress.* |
Execution or intake adapter | Feature bridge |
*.Extensions.* |
Framework or host composition adapter | Host adapter |
Name new packages to match an existing role before inventing a new shape. The aggregate LiteBus meta-package is the only kitchen-sink reference; all other integrations stay opt-in.
LiteBus splits NuGet packages along concern boundaries so consumers reference only what they run. Many packages look small (for example a single UseAmqpDispatch registration file); that thin surface is deliberate wiring, not an invitation to merge assemblies for fewer package IDs.
Default rule: preserve one installable package per orthogonal concern. Do not collapse, combine, or meta-bundle adapters unless the user or a maintainer explicitly approves a breaking packaging change.
| Split | Why it stays separate |
|---|---|
| Inbox vs outbox | Different durable semantics (accept/execute vs enqueue/publish), processors, store roles, and operational paths. Services often need one axis only; they scale and evolve on different timelines. |
| Dispatch vs ingress vs storage | Storage is persistence; dispatch is outbound execution after lease; ingress is broker intake into accept. A read-only publisher may use outbox dispatch without inbox ingress or storage on the same host. |
| Inbox.Dispatch.* vs Outbox.Dispatch.* | Same broker, different envelope contracts and mediators (ICommandMediator vs IEventMediator). Installing Inbox.Dispatch.Amqp must not imply Outbox.Dispatch.Amqp or its transitive graph. |
Per broker (*.Dispatch.Amqp, *.Ingress.Kafka, Transport.Amqp) |
Each broker pulls its own SDK. A Kafka-only service must not transitively reference RabbitMQ, Azure Service Bus, or AWS clients. |
Per store (*.Storage.PostgreSql, *.Storage.EntityFrameworkCore, *.Storage.InMemory) |
Same rule for ORM and database drivers. |
Shared core vs broker glue (Inbox.Dispatch + Inbox.Dispatch.Amqp) |
Shared dispatch logic stays broker-neutral; glue packages register one IModule pair without referencing sibling brokers or the other durable axis. |
Per-module *.Extensions.Microsoft.DependencyInjection / *.Extensions.Autofac |
Empty reference-only install shells; one NuGet ID per semantic module for opt-in registration. Keep these packages separate. |
Consumer composition follows need, not defaults:
Inbox only, AMQP dispatch, PostgreSQL storage
-> Inbox (+ abstractions via module)
-> Inbox.Storage.PostgreSql
-> Inbox.Dispatch.Amqp (not Outbox.Dispatch.Amqp)
-> optional Inbox.Ingress.Amqp (only when consuming from a broker)
Outbox only, in-process dispatch
-> Outbox
-> Outbox.Storage.*
-> Outbox.Dispatch.InProcess (no inbox packages)
What agents should not propose by default
- Removing or merging per-module empty
*.Extensions.Microsoft.DependencyInjectionor*.Extensions.Autofacshell packages. - Merging inbox and outbox adapters into one package because they share similar file names.
- A single
UseTransport(TransportKind, ...)API backed by one assembly that references everyTransport.*broker (forces unused SDKs onto consumers). - "Durable" or "Transport" meta-packages that bundle both axes and multiple brokers for convenience (duplicates the forbidden kitchen-sink pattern outside the documented
LiteBus/Extensions.*entry points). - Treating high package count in
site/content/docs/architecture/dependency-graph.mdas technical debt; treat unwanted transitive dependencies as the debt signal instead.
When consolidation is in scope
- The user explicitly asks to reduce package count and accepts breaking reference changes.
- Two packages always ship together, share identical versioning constraints, and never appear independently in samples or consumer apps (rare; needs evidence).
- Extracting shared implementation into an allowed core or technology package without changing which packages consumers must install (refactor, not merge).
Per-module empty DI/Autofac extension shells are not consolidation candidates unless a maintainer explicitly requests their removal in a tracked packaging change.
Ergonomic aliases belong in documentation and samples, not in wider default dependency graphs. See Dependency Graph for the living inventory.
- Vertical domain packages (durable messaging, saga, semantic mediators) must not reference each other or broker or ORM SDKs unless the dependency rule table in
site/content/docs/architecture/dependency-graph.mdexplicitly allows it. - Horizontal platform packages (runtime, transport) must not reference vertical domain abstractions.
- Mapping between axes (domain envelope to wire format, store row to contract) belongs in feature bridges, not platform core.
Packages ending in .Abstractions contain only interfaces, value objects, enums, exceptions, attributes, and coordination types whose fields and parameters are abstract types. They never reference concrete implementation, storage, transport, or hosting packages.
Abstractions stay abstract on public surfaces. Parameters, return types, properties, and fields exposed by *.Abstractions types must be interfaces, primitives, enums, records, or other types from allowed contract roles. Do not surface concrete store, transport, ORM, broker SDK, or hosting types in public or internal API members consumers could depend on indirectly.
Public APIs group related parameters into semantic types named by role, not by parameter count. Match an existing suffix before inventing a new one. Concrete inventories, package maps, feature exemplars, CLR kind selection, and the inbox/outbox Message property rule live in API Design; this section states the rules that apply to every axis.
| Role | Purpose | Typical suffix |
|---|---|---|
| Command input | What the caller intends for one operation | *Item, sometimes *Request |
| Per-operation annotations | Metadata on a command, composed from value objects | *Metadata |
| Invocation tuning | How one call behaves (mediation, optional second parameter) | *Settings |
| Host and module configuration | Registration-time behavior for processors, stores, adapters | *Options, *HostOptions |
| Query predicate | What to filter or purge | *Filter |
| Persistence row | What storage holds after mapping | Envelope or entity types (sparse fields allowed) |
| Small identifier | Shared, stable value object | No suffix |
Do not merge command input, invocation tuning, and module configuration into one type.
| Suffix | Use when | Do not use for |
|---|---|---|
*Item |
One atomic command: payload plus per-operation metadata | Host config, parallel parameter lists |
*Metadata |
Annotations on an *Item, built from grouped value objects |
Module registration, mediation tuning |
*Request |
Operation input without a domain payload body | Full message accept or enqueue commands |
*Settings |
Per-invocation pipeline tuning on mediators | Stored message metadata |
*Options |
Module, processor, adapter, or store configuration | Per-message writer input |
*HostOptions |
Background-service lifecycle only | Business commands |
*Filter |
Query or purge predicates where optional fields are intentional | Writer commands |
*Binding |
Framework adapter HTTP or host binding input at the edge | Writer commands, mediation settings |
| (no suffix) | Small identifiers and cross-cutting value objects | Bags of nullable primitives |
Banned suffixes and shapes
- Do not introduce
*Specificationtypes. In domain-driven design that suffix means a selection predicate; use plain concept names with nested variants instead. - Do not use
*Optionsfor per-message or per-command writer input. - Do not expose parallel aligned parameter lists that must stay length-matched. Use
IReadOnlyList<*Item>. - Do not embed
CancellationTokeninside*Item,*Metadata, or*Request. Pass cancellation as the final method parameter only. - Do not add writer or store overloads with two or more business parameters beyond the message body once an
*Itemor*Requestexists. Optional metadata, scheduling, routing, and identity belong on*Item/*Metadata, not on parallel scalar parameters.
When a command carries optional behavior, group by concern inside *Metadata using discriminated value objects (explicit variants, not nullable "absent" fields):
| Concern | Typical variants |
|---|---|
| Identity | generated vs caller-supplied identifier |
| Idempotency | none vs keyed |
| Scheduling or visibility | immediate vs at absolute time vs after delay |
| Trace or correlation | none vs correlated vs workflow vs distributed context |
| Tenancy | unscoped vs isolated tenant |
| Routing or target | default vs explicit destination (axis-specific) |
Feature packages name their own value objects. Shared cross-axis primitives belong in the narrowest contract role that both axes may reference. See API Design for the durable-messaging application.
- Command boundary: prefer abstract record hierarchies with named variants (
None,Generated,Supplied) over nullable properties. - Persistence boundary: sparse nullable columns are allowed on envelopes and store rows.
- Query boundary: optional predicates on
*Filtertypes are allowed. - External input: broker or framework payloads may be nullable; map once at the adapter edge.
One mapper per feature owns translation from command value objects to persistence shape. Adapters translate external formats into command metadata; they do not duplicate mapping logic at the store.
- 0-2 business parameters: inline parameters are acceptable when each is orthogonal.
- 3 or more business parameters: introduce or extend an
*Itemor*Requestbefore adding another parameter. - Batch operations: accept
IReadOnlyList<*Item>, not params arrays or parallel lists of related values. - Async methods: semantic input first,
CancellationTokenlast. - Mediators: message plus optional
*SettingsplusCancellationTokenremains the established pattern.
- Shared value objects used by multiple axes sit in the narrowest shared
*.Abstractionspackage allowed by the role matrix. - Axis-specific command types (
*Item,*Metadata, receipts) sit in that axis's*.Abstractions. - Mediation
*Settingssit in the matching semantic module abstractions package. - Module and host
*Optionssit in the package that registers the service. - Internal projection helpers (command to persistence) stay
internalin the core implementation package for that feature. - Adapters map and wire; they do not define alternate option bags for concerns already modeled in abstractions.
- Prefer static factories on
*Itemrecords (OutboxEnqueueItem<T>.From(message)) so construction stays discoverable on the type callers pass to writer APIs. - Writer facades may expose one body-only sugar overload per operation family (for example
EnqueueAsync<TEvent>(TEvent message, ...)implemented asEnqueueAsync(OutboxEnqueueItem<TEvent>.From(message), ...)). Do not add further overloads that take metadata scalars. - Use
withon*Itemand*Metadatafor ad-hoc composition. Add named static helpers on the*Itemrecord only when nestedwithis repetitive and domain-named (ScheduledAt,WithTopic). - Optional thin static helper types (
OutboxEnqueue,InboxAccept) are acceptable for cross-shape glue (untyped batch entries). Do not use plural*Itemsnames that imply a collection type. Avoid no-op batch wrappers that only return a passed array. - Keep compose-time
*Optionson module builders. Do not thread them through runtime command methods. - When a workflow needs both mediation and durable persistence, use separate calls with separate semantic types; do not merge the concerns.
- Is this command, configuration, query, persistence, or external adapter input?
- If command: is there a single
*Itemor*Request? - Are optional concerns expressed as variants, not nulls?
- Are there more than two business parameters? If yes, collapse to one semantic type.
- Is
CancellationTokenon the method only? - Does mapping live in one place?
- Does the suffix match the taxonomy table?
- Does the type belong in abstractions or an adapter package per dependency role rules?
Surfaces that do not match this taxonomy should be aligned when their area is next touched: options objects that mix invocation tuning with cancellation or strategy references; methods with three or more scalar parameters where a request record would clarify intent; error and lease APIs that pass parallel context fields instead of a context record. Specific type names, CLR kind rules (sealed record vs sealed class for *HostOptions), *Binding adapter types, and target shapes are tracked in API Design. Historical mappings belong in the Migration Guides.
Modules with sub-modules implement ICompositeModule. DeclareChildren runs during Register() before any Build(), and the builder action runs inside DeclareChildren. Build() registers core services only. Required ordering is expressed through IRequires<TModule> and composite ownership; modules do not use registration markers or scan the registry during Build(). The registry inserts children depth-first, validates the complete dependency graph, and then topologically sorts it before building any module. Duplicate registration of the same module type throws LiteBusConfigurationException at compose time.
Compose through parent module builders. Register storage, dispatch, and ingress inside the parent module builder via Use* extensions. Do not add new top-level IModuleRegistry shortcuts that bypass the parent builder. Mark obsolete patterns rather than extending them.
- Applications reference only packages they compose. See Granular opt-in packages above; never widen a package reference graph because another integration exists in the same repo.
- Do not add convenience APIs on shared builders that pull storage, transport, or other adapters into generic DI packages.
- Defer ergonomic shortcuts to
site/content/docs/roadmap/README.mdwhen they would violate role boundaries or opt-in packaging.
- One adapter package per integration surface (one store technology, one broker, one host framework).
- Adapters register through
IModuleConfiguration; they do not registerIHostedService,IHealthCheck, or equivalent framework types directly. - Observability and diagnostics use framework-neutral contracts or dedicated host-adapter
*.Extensions.*packages.
- One-shot host startup work implements
IStartupTaskwithTask RunAsync(CancellationToken cancellationToken)and registers throughconfiguration.RegisterStartupTask(Type). - Long-running host loops implement
IBackgroundServicewithTask ExecuteAsync(CancellationToken stoppingToken)and register throughconfiguration.RegisterBackgroundService(Type). - Modules register services with
configuration.DependencyRegistry.Register(DependencyDescriptor). - Do not put
RegisterBackgroundServiceorRegisterStartupTaskonIDependencyRegistry. DI adapters must not referenceMicrosoft.Extensions.Hosting. - Generic host bridging lives in
LiteBus.Runtime.Extensions.Microsoft.HostingandLiteBus.Runtime.Extensions.Autofac.Hosting, applied fromAddLiteBusafter module build. - Diagnostic probes implement
IDiagnosticCheckand register throughconfiguration.RegisterDiagnosticCheck(Type, string).AddLiteBusexposes collected descriptors onLiteBusHostManifest. - Manifest over direct host wiring. Any work the generic host must run (startup tasks, background loops, diagnostic probes) registers through
IModuleConfigurationmanifest methods. Core and adapter packages must not register host framework types directly.
See site/content/docs/architecture/hosted-services.md for registration examples and feature-specific hosted types.
IContractWriterfor module builders at configuration time.IContractReaderfor dispatchers and envelope factories at runtime.IMessageContractRegistryextends both and is the DI singleton key.MessageContractBuilderdefers registrations until the parent moduleBuild().[MessageContract]is read at runtime viaAddFromAssemblyand on-demand inGetContract.
IMessageWriterfor module builders at configuration time (Register(Type)).IMessageReaderfor the mediator and resolve strategies at runtime (Find, enumeration,Handlers,Count).IMessageRegistryextends both and is the DI singleton key; created once perIModuleConfigurationinMessageModule.Build()viaGetOrCreateContext.- Do not use a process-wide static accessor or
Clear(); tests use a newMessageRegistryinstance per case.
- Telemetry meter names, activity source names, and instrument name constants on public telemetry types are part of the consumer contract. Treat renames and removals as breaking changes.
- When adding instruments, define names as public
const stringon the telemetry type, register meters through the matching host-adapter*.Extensions.OpenTelemetrypackage, and document new names insite/content/docs/architecture/README.md. - Builder method renames, manifest entry changes, and persisted envelope field semantics are breaking; update docs in the same change.
- Prefer stable contract names and versions over assembly-qualified CLR names in persisted envelopes.
Keep each package's dependency graph minimal and aligned with its dependency role. Before adding a NuGet or project reference, confirm the consuming code uses a type or member that truly requires it.
- Prefer BCL and abstractions already in the graph.
IServiceProvider.GetService(Type)lives inSystem; do not addMicrosoft.Extensions.DependencyInjection*packages only to callGetService<T>()or other extension methods. Use the non-generic API or inject the required service through constructors and module registration instead. - Restrict hosting and framework packages to host adapters. Other roles must not reference
Microsoft.Extensions.Hosting,Microsoft.Extensions.Diagnostics.HealthChecks, ASP.NET Core, or similar host frameworks. - One integration surface per concern. Observability, health, and diagnostics belong in framework-neutral contracts (
IDiagnosticCheck, OpenTelemetry meters on public constants) or in dedicated*.Extensions.*adapter packages. - Justify every new reference in review. If a feature can be expressed with an existing abstraction, manifest entry, or documented application code, prefer that over a new package dependency.
Before adding a project:
- Which dependency role and role suffix?
- Can an existing package absorb this without a forbidden role edge?
- Does it need a new
*.Abstractionspackage or fit an existing one? - Does it need manifest registration (startup task, background service, diagnostic check)?
- Does
site/content/docs/architecture/dependency-graph.mdneed a new row? - Does
site/content/docs/architecture/README.mdneed a feature section or invariant note?
- Ship compile-time rules in
LiteBus.Analyzersonly; no runtime dependency on mediator or durable packages. - Keep the rule inventory in
site/content/docs/reference/analyzers.mdaligned withDiagnosticIds(LB1001-LB1017). - LB1007 covers handled durable types missing contract registration; honor
RegisterFromAssemblythe same as explicitContracts.Register. - LB1017 covers attributed durable types; match only
IContractWriter/IMessageContractRegistryRegisterinvocations, not unrelatedRegister<T>()methods. - LB1004 must cover
AcceptAsync,AcceptBatchAsync, andITransactionalInboxacceptance APIs. - Add or update analyzer tests in
tests/LiteBus.Analyzers.Testswhen changing diagnostic behavior.
- Run
dotnet build LiteBus.slnxaftersrc/edits. - Strong-named assemblies:
InternalsVisibleTomust include the solution public key (seesrc/Directory.Build.props). - Central package versions: add NuGet packages to
src/Directory.Packages.props, not inline in csproj files. - New test projects using xUnit attributes: add
GlobalUsings.cswithglobal using Xunit;when the project has no other global usings for xUnit.
- Tests prove behavior, not wiring. Prefer assertions on outcomes (manifest contents, probe results, processor pass behavior, store state transitions) over tests that only verify a type appears in dependency injection or a module builds without exception.
- Use a fresh
MessageRegistry(or isolated module configuration) per test case; do not rely on process-wide static state. - Add regression tests when fixing manifest ordering, diagnostic registration, telemetry recording, or composite module child expansion.
- Docs move with API changes. When adding, renaming, or removing public builder methods, contracts, manifest entries, metric names, or registration patterns, update the matching section in
site/content/docs/in the same change (Architecture.md,API-Design.md,Hosted-services.md, feature guides, orRoadmap.mdwhen scope shifts). - Document application-owned integration (health endpoints, schema probes, export sinks) as recipes; ship framework-neutral contracts and stable telemetry names in libraries.
- Keep
site/content/docs/architecture/hosted-services.mdandsite/content/docs/architecture/README.mdaligned with the manifest model (IStartupTask,IBackgroundService,IDiagnosticCheck,LiteBusHostManifest). - Keep
site/content/docs/architecture/dependency-graph.mdas the living package inventory and dependency rule reference.
These instructions and site/content/docs/ should reflect how the codebase is actually built. Agents and maintainers may change any rule when the project direction shifts. Propose amendments in the same PR or conversation as the code change that motivates them, so guidance and implementation stay aligned.