HostLoom.Pipelines.DependencyInjection 0.3.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package HostLoom.Pipelines.DependencyInjection --version 0.3.0
                    
NuGet\Install-Package HostLoom.Pipelines.DependencyInjection -Version 0.3.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="HostLoom.Pipelines.DependencyInjection" Version="0.3.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="HostLoom.Pipelines.DependencyInjection" Version="0.3.0" />
                    
Directory.Packages.props
<PackageReference Include="HostLoom.Pipelines.DependencyInjection" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add HostLoom.Pipelines.DependencyInjection --version 0.3.0
                    
#r "nuget: HostLoom.Pipelines.DependencyInjection, 0.3.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package HostLoom.Pipelines.DependencyInjection@0.3.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=HostLoom.Pipelines.DependencyInjection&version=0.3.0
                    
Install as a Cake Addin
#tool nuget:?package=HostLoom.Pipelines.DependencyInjection&version=0.3.0
                    
Install as a Cake Tool

HostLoom

Language .NET SDK License

HostLoom — an experimental, Spring-inspired application framework for .NET 10 and C# 14. The first vertical slice is typed request/response messaging over interchangeable transports, carried by a transport-neutral asynchronous middleware pipeline. The current slice implements:

  • generic IPipe<TContext> and IFilter<TContext> composition, with typed context payloads, conditional branches, retry, circuit breaking, rate and concurrency limits, timeouts, intentional short-circuits, and immutable pipeline probes;
  • dependency-injection pipeline registration with named stages, per-run filter resolution, feature toggles, startup validation, built-in per-filter metrics and tracing, and a deterministic test harness;
  • explicit, compile-time-safe object maps with scoped dependency-injection dispatch and reflection-free, Native AOT-compatible map dispatch;
  • typed IRequest<TResponse> contracts with handler, behavior, and client abstractions;
  • typed IEvent contracts published to a topic and fanned out to named subscriptions;
  • a configurable receive pipeline wrapping handler execution on every transport;
  • one dependency-injection scope per delivery attempt;
  • explicit wire envelopes carrying message id, correlation id, logical type name, timestamp, and remote faults;
  • a configurable System.Text.Json serialization boundary;
  • an OpenTelemetry-compatible ActivitySource and Meter, both named HostLoom;
  • liveness and readiness health checks, and an execution-free pipeline probe;
  • in-memory, RabbitMQ, and Kafka broker adapters;
  • an authenticated raw-WebSocket RPC and live-subscription gateway with JSON, MessagePack, and Protocol Buffers subprotocols, bounded per-connection memory, and explicit credit;
  • .NET Generic Host startup with graceful endpoint disposal;
  • tests for typed round trips, behavior ordering, and fault propagation.

HostLoom is intentionally a small foundation, not a MassTransit reimplementation. It borrows two durable ideas: GreenPipes' composable asynchronous pipeline becomes the transport-neutral HostLoom.Pipelines package, and MassTransit's typed contracts, scoped consumers, correlation, faults, and hosted transport lifecycle become a compact request runtime.

Packages

Runtime packages target .NET 10, the compiler-hosted analyzer targets netstandard2.0, and all packages are versioned together:

Package Purpose
HostLoom.Analyzers Compile-time checks for asynchronous, DI, and mapping usage
HostLoom Typed request/response and event runtime
HostLoom.Pipelines Transport-neutral asynchronous pipelines
HostLoom.Pipelines.DependencyInjection Named stages, per-run resolution, and instrumentation
HostLoom.Pipelines.Testing Deterministic pipeline test doubles and harnesses
HostLoom.Transport.InMemory In-process request and event transport
HostLoom.Transport.RabbitMq RabbitMQ request and fan-out event transport
HostLoom.Transport.Kafka Kafka request and consumer-group event transport
HostLoom.AspNetCore.WebSockets Authenticated WebSocket RPC and subscriptions
HostLoom.Logging Allocation-free UTF-8 logging provider
HostLoom.Diagnostics Composition ledger and startup report of registration decisions
HostLoom.Mapping Explicit, compile-time-safe, AOT-friendly object mapping
HostLoom.Mapping.DependencyInjection Scoped mapper dispatch and explicit map registration
HostLoom.Mapping.Testing Container-free mapper composition for tests

Install only the runtime and transport needed by the application, for example:

dotnet add package HostLoom.Transport.RabbitMq --version 0.2.0

The analyzer package is optional and has no runtime dependency:

dotnet add package HostLoom.Analyzers

It reports an omitted available cancellation token (HLM0001), synchronous blocking over a HostLoom async operation (HLM0002), singleton registration of handlers or behaviors that should follow HostLoom's per-delivery scope (HLM0003), a destination member an explicit map never assigns (HLM0004) and a map body whose completeness cannot be verified (HLM0005), and the scoped mapping dispatcher captured in a singleton (HLM0006). See the analyzer rule reference.

Quick start

using HostLoom;
using HostLoom.Transport.RabbitMq;

var builder = Host.CreateApplicationBuilder(args);

builder.Services
    .AddHostLoom(options => options.RequestTimeout = TimeSpan.FromSeconds(10))
    .UseRabbitMq(options => options.Uri = new Uri("amqp://guest:guest@localhost:5672/"))
    .AddHandler<GetGreeting, Greeting, GetGreetingHandler>("greetings");

await builder.Build().RunAsync();

public sealed record GetGreeting(string Name) : IRequest<Greeting>;
public sealed record Greeting(string Text);

public sealed class GetGreetingHandler : IRequestHandler<GetGreeting, Greeting>
{
    public ValueTask<Greeting> HandleAsync(GetGreeting request, CancellationToken cancellationToken) =>
        ValueTask.FromResult(new Greeting($"Hello, {request.Name}!"));
}

A client-only application registers the contract without a local handler:

builder.Services
    .AddHostLoom()
    .UseRabbitMq()
    .AddRequestClient<GetGreeting, Greeting>();

var client = services.GetRequiredService<IRequestClient<GetGreeting, Greeting>>();
var reply = await client.GetResponseAsync("greetings", new GetGreeting("Ada"), cancellationToken: stoppingToken);

Publish and subscribe

A request addresses one handler and expects one reply. An event is published to a topic and delivered to every subscription on it:

builder.Services
    .AddHostLoom()
    .UseInMemory()
    .AddSubscriber<OrderPlaced, AuditHandler>("orders", subscription: "audit")
    .AddSubscriber<OrderPlaced, ShippingHandler>("orders", subscription: "shipping");

await publisher.PublishAsync("orders", new OrderPlaced("A-1"));

public sealed record OrderPlaced(string Reference) : IEvent;

Two subscription names on one topic each receive every event. Two handlers under the same name share one delivery and one dependency-injection scope. A subscription that has no handler for a published contract ignores it rather than failing.

Publish/subscribe is a separate transport capability, IEventBroker. Publishing through a transport that lacks it throws, and registering a subscription against one fails at startup rather than starting up looking subscribed while nothing is delivered.

Each transport maps a subscription onto its own fan-out primitive:

  • In-memory — a named handler on the topic, delivered to in process.
  • RabbitMQ — a fanout exchange per topic, and a durable queue named topic.subscription bound to it, so subscriptions accumulate their own backlog rather than competing for one queue. Events publish with no routing key and without mandatory, so an event nobody subscribes to is dropped instead of failing the publish.
  • Kafka — the topic is a Kafka topic and each subscription is its own consumer group, so every group receives every record while instances sharing a group divide the partitions. Records are produced without a key, so ordering holds within a partition only.

Receive pipeline

Filters registered with ConfigureReceivePipeline wrap handler execution for every inbound delivery — requests and events alike — on every transport:

builder.Services
    .AddHostLoom()
    .UseRabbitMq()
    .ConfigureReceivePipeline(pipe =>
    {
        pipe.UseRetry(RetryPolicy.Exponential(3, TimeSpan.FromMilliseconds(200), TimeSpan.FromSeconds(5)));
        pipe.UseCircuitBreaker(failureThreshold: 5, resetInterval: TimeSpan.FromSeconds(30));
    })
    .AddHandler<GetGreeting, Greeting, GetGreetingHandler>("greetings");

These filters see a handler failure as an exception, before it is encoded as a fault envelope, which is what makes retry and circuit breaking apply to it. The wire contract is unchanged: an exhausted retry or an open breaker still reaches the caller as a RemoteRequestException.

Each attempt runs in its own dependency-injection scope, so a retry never inherits scoped state left behind by the attempt that failed. The pipeline is composed once, so a circuit breaker or rate limiter shares state across every delivery rather than resetting per message.

Retrying in process is a different thing from broker redelivery. This pipeline never moves a broker offset; redelivery is the transport's concern.

A filter receives a ReceiveContext, which is a RequestReceiveContext or an EventReceiveContext. Both carry Destination, MessageType, and Message; the event form adds Subscription. One pipeline serves both, so a breaker tripped by failing requests also rejects events — a single verdict on whether this process should be taking work.

Raw WebSocket gateway

HostLoom.AspNetCore.WebSockets exposes an explicit subset of request operations and event topics to WebSocket clients without SignalR:

builder.Services
    .AddHostLoom()
    .UseRabbitMq()
    .AddWebSocketGateway()
    .AddRequest<GetOrder, OrderView>("orders.get", "orders-api", "orders.read")
    .AddTopic<OrderChanged>("orders.changed", "orders", e => e.CustomerId,
        subscription: "realtime-node-a", authorizationPolicy: "orders.read");

app.UseAuthentication();
app.UseAuthorization();
app.UseHostLoomWebSockets();
app.MapHostLoomWebSocketHub("/realtime");

Clients negotiate hostloom.msgpack.v1, hostloom.protobuf.v1, or hostloom.json.v1. Authentication happens before upgrade and named ASP.NET Core policies are checked again for every operation and subscription. One receive loop, one socket writer, a byte-bounded outbound queue, concurrent-request limits, and subscription credit prevent a slow client from creating unbounded per-connection work or memory.

The initial subscription protocol is live and process-local: acknowledgements record progress but do not provide replay, and gateway-generated event IDs are not broker offsets. Multi-node services must choose distinct broker subscription names per node or add a fan-out/backplane according to the broker's actual queue or consumer-group semantics. See the gateway protocol and operating notes.

Codec encode/decode throughput and allocations are measured separately with BenchmarkDotNet:

dotnet run --project benchmarks/HostLoom.Benchmarks -c Release -- --filter "*WebSocketProtocol*"

The suite covers zero-byte, 256-byte, and 4 KiB payloads. A --job Dry run verifies benchmark discovery and execution but is not statistically meaningful.

Health and metrics

AddHealthChecks() registers two checks, tagged live and ready so they map to separate probe endpoints:

builder.Services.AddHostLoom().UseRabbitMq().AddHealthChecks();

app.MapHealthChecks("/health/live", new() { Predicate = r => r.Tags.Contains("live") });
app.MapHealthChecks("/health/ready", new() { Predicate = r => r.Tags.Contains("ready") });

Liveness never contacts the broker. It answers "should this process be restarted", and a broker outage answering yes turns one outage into a restart storm across every pod that talks to it. Broker reachability belongs in readiness, which reports unhealthy when endpoints are not listening or the transport says the broker is unreachable.

A transport reports reachability by implementing IBrokerHealthProbe. One that does not is treated as reachable, because "cannot tell" must not read as "broken".

Metrics are published on a Meter named HostLoom, tagged by destination and message type: hostloom.request.duration, hostloom.request.active, hostloom.request.faults, and hostloom.request.retries.

HostLoomProbe returns the receive pipeline's structure without executing it, which suits a debug endpoint:

app.MapGet("/diagnostics/pipeline", (HostLoomProbe probe) => probe.ReceivePipeline());

Explicit object mapping

HostLoom.Mapping is a compile-time-safe alternative to runtime convention mapping. Each map is an ordinary class, so constructors, required members, conversions, and nullability remain visible to C# and code review. The core package is dependency-free; the DI adapter uses closed generic registrations without assembly scanning, reflection-based member mapping, expression compilation, or runtime code generation:

using HostLoom.Mapping;
using HostLoom.Mapping.DependencyInjection;

builder.Services.AddHostLoomMapping(mapping =>
    mapping.Add<CustomerMapper>());

public sealed class CustomerMapper : IMapper<Customer, CustomerDto>
{
    public CustomerDto Map(Customer source) => new(source.Id, source.Name.Trim());
}

The pair is read from the interface the map class already declares, so a registration restates nothing and the file that registers a map needs no using for the contracts it maps between. A generic map class is closed through a factory overload, which is what registers many pairs from one class.

Inject IMapper<Customer, CustomerDto> into a component that needs one pair — that is the shape to reach for, and the fastest. Orchestration code coordinating several pairs can inject the scoped IMapper dispatcher and write mapper.From(customer).To<CustomerDto>(). Map classes are transient by default, duplicate pairs fail at registration, and a missing pair throws MappingNotFoundException naming both types and what the source is registered to map to.

Sequences and null are extension methods whose names carry the policy — MapMany rejects a null source, MapManyOrEmpty treats it as empty, MapOrNull maps one value through null — so every place that depends on null tolerance stays greppable, where a convention mapper decides it once and invisibly. HostLoom.Analyzers reports a destination member a map never assigns (HLM0004) and a map body it cannot verify (HLM0005), which is what keeps a forgotten member from shipping as silent data loss.

Mapping is deliberately synchronous and performs no I/O. Fetch and enrich data outside a map, use distinct destination types for distinct semantic views, and write database projections directly as IQueryable.Select expressions. All three mapping packages enable the .NET SDK Native AOT and trimming analyzers.

Logging

HostLoom.Logging is a structured Microsoft.Extensions.Logging provider: a bounded queue with a dedicated background writer, typed JSON fields from ordinary ILogger template holes and from the allocation-free LogFast interpolated path, Serilog-compatible {@...} destructuring with fail-closed [NotLogged]/[LogMasked] protection, scopes, enrichers, and health metrics on the HostLoom.Logging meter.

builder.Logging.AddHostLoomLogging(
    StreamLogSink.Console(),
    builder.Configuration.GetSection("HostLoom:Logging"),
    formatter: new ClefLogFormatter());

Level filtering is standard MEL configuration and runs before the provider — HostLoom does no level filtering of its own. Migrating from Serilog's section, MinimumLevel:Default becomes Logging:LogLevel:Default and each MinimumLevel:Override:<prefix> becomes Logging:LogLevel:<prefix>. Provider options bind from HostLoom:Logging; a code callback, when supplied, applies after configuration, and invalid values fail at host startup:

{
  "Logging": { "LogLevel": { "Default": "Information", "Npgsql": "Warning" } },
  "HostLoom": {
    "Logging": {
      "QueueCapacity": 8192,
      "QueueFullPolicy": "DropBelowWarning",
      "EnqueueTimeout": "00:00:02",
      "ShutdownTimeout": "00:00:05",
      "ServiceName": "checkout",
      "Destructuring": { "MaxDepth": 5, "MaxStringLength": 4096 }
    }
  }
}

Before the host exists, HostLoomBootstrapLogger writes the same event shape synchronously to stdout — same formatter, masking policy, timestamps, and static fields — with a minimum level supplied at construction. Dispose it once the hosted provider is up; it retains nothing, so the hand-off neither replays nor duplicates events.

Composition diagnostics

Registration builds a plan and executes nothing, so the moment a branch is taken is the one moment with no logger, no bound options, and no filter configuration — which is why composition logging so often ends up as a static Log.Debug call that fires before the real sinks and levels exist. HostLoom.Diagnostics inverts that: registration records its decisions into a ledger, and the whole plan is reported once at startup, through the application's own logging stack.

using HostLoom.Diagnostics;

public static IServiceCollection AddOrderPublishing(
    this IServiceCollection services, OrderOptions options)
{
    if (options.Kafka.Enabled)
    {
        services.AddSingleton<IOrderPublisher, KafkaOrderPublisher>();
        services.RecordComposition("OrderPublisher", "Kafka", "Orders:Kafka:Enabled=true");
    }
    else
    {
        services.AddSingleton<IOrderPublisher, InProcessOrderPublisher>();
        services.RecordComposition("OrderPublisher", "InProcess", "Orders:Kafka:Enabled=false");
    }

    if (options.Outbox is null)
    {
        services.RecordSkippedComposition("Outbox", "no Orders:Outbox section bound");
    }

    return services;
}

AddCompositionDiagnostics() turns on the report; without it nothing is written, so a library can record unconditionally. The opt-in may appear anywhere in the composition root, because the report is taken when the host starts rather than when diagnostics are switched on:

builder.Services.AddCompositionDiagnostics();
info: HostLoom.Diagnostics.Composition
      HostLoom composition: OrderPublisher=Kafka | Outbox=(skipped) | Scheduler=Quartz

One Information line carries the whole manifest, because a composition question is asked when production misbehaves — exactly when a Debug line has already been filtered out. Debug adds one line per decision with its reason and the registration method that recorded it, captured automatically from the call site. A component recorded twice with choices that disagree raises a Warning naming both, without guessing which one the container resolved. Everything is written under the HostLoom.Diagnostics.Composition category, so standard Logging configuration raises, lowers, or silences it.

Recording a component that was skipped is the part worth the discipline: a log that only reports what was registered cannot answer "what is missing", because the branch that did nothing wrote nothing. Nothing else recovers that.

Use it selectively. A branch whose input is already visible in configuration rarely earns an entry; the ones that do are components deliberately left out, and branches whose mapping from configuration to registration is not a direct toggle. Nothing enforces the calls, so an entry left behind by a branch that changed will misreport — keep each one next to the registration it describes, and prefer no entry to a stale one.

The report is a plan, not a validation, and it is not the cheapest tool for every question. Keep ValidateOnBuild and ValidateScopes on in development for unresolvable dependencies, ValidateOnStart on options for settings that are absent or invalid, and ((IConfigurationRoot)builder.Configuration).GetDebugView(…) for which provider supplied which value. Each of those is one line and stays correct on its own; the ledger is the one that asks something of you in return.

The package stands alone and no other package depends on it, so an application that never references it carries nothing. Pipeline topology is not recorded here — pipeline registration logs each resolved topology itself when the host starts.

CompositionLedger and its Snapshot() are public, so a test can assert on what registration decided instead of capturing log output to observe it.

Transports

A request address is a logical name; each adapter maps it onto its own honest transport topology.

  • .UseInMemory() — deterministic in-process delivery, for tests and local composition.
  • .UseRabbitMq(...) — the address becomes a durable request queue. Each client opens an exclusive reply queue and correlates replies through the AMQP CorrelationId and ReplyTo properties.
  • .UseKafka(...) — the address becomes a request topic. Replies go to KafkaOptions.ResponseTopic and correlation travels in Kafka headers. The response topic must be provisioned with enough retention for the maximum request timeout.

The Kafka adapter gives every client instance a unique response consumer group, so each instance sees the shared response stream and ignores responses it does not own. That is correct for an initial implementation but not the final high-scale topology; partition-affine reply routing is on the roadmap.

RabbitMQ and Kafka are not hidden behind an identical topology because they do not have identical semantics. RabbitMQ naturally supports an exclusive reply queue and AMQP correlation. Kafka is a durable partitioned log, so request/reply is an application protocol built from request topics, response topics, keys, headers, retention, and consumer groups. HostLoom keeps the application API common while letting each adapter own its transport protocol.

Generic pipelines

HostLoom.Pipelines is the GreenPipes-inspired foundation and has no dependency on messaging or a broker. Filters execute in registration order and decide whether to invoke the next stage:

using HostLoom.Pipelines;

var pipeline = Pipe.Create<CommandContext>(pipe =>
{
    pipe.UseConcurrencyLimit(32);
    pipe.Use(async (context, next) =>
    {
        var stopwatch = context.GetOrAddPayload(() => Stopwatch.StartNew());
        await next.SendAsync(context);
        Console.WriteLine(stopwatch.Elapsed);
    }, "timing");
    pipe.UseWhen(
        context => context.Audited,
        branch => branch.UseExecute(context => WriteAudit(context)));
});

await pipeline.SendAsync(new CommandContext(cancellationToken));
var structure = PipelineProbe.Inspect(pipeline);

sealed class CommandContext(CancellationToken cancellationToken) : PipeContext(cancellationToken)
{
    public bool Audited { get; init; }
}

Use UseTerminal for an intentional short circuit. Context payloads are lazy and thread-safe, and can be retrieved through an implemented interface. PipelineProbe.Inspect returns an immutable tree suitable for health endpoints and diagnostics.

Resilience is composed the same way, from filters rather than from a separate policy engine:

pipe.UseRetry(RetryPolicy.Exponential(3, TimeSpan.FromMilliseconds(200), TimeSpan.FromSeconds(5)).WithJitter(0.2));
pipe.UseCircuitBreaker(failureThreshold: 5, resetInterval: TimeSpan.FromSeconds(30));
pipe.UseRateLimit(limit: 100, interval: TimeSpan.FromSeconds(1));

UseRetry re-invokes the rest of the pipeline, exposing the attempt as a RetryAttempt payload and never retrying cancellation. UseCircuitBreaker throws CircuitBreakerOpenException once the downstream has failed failureThreshold times in a row, then admits one trial call per resetInterval. UseRateLimit shapes throughput by waiting rather than throwing. All of them accept a TimeProvider, so their timing is testable without real waiting.

UseTimeout bounds the remainder of the pipeline for contexts deriving from PipeContext — a compile-time constraint, because the filter swaps a linked token into the context for the duration of the downstream call. Filters that honour context.CancellationToken stop promptly, the run fails with PipelineTimeoutException, and caller cancellation is always rethrown as cancellation, never misreported as a timeout:

pipe.UseTimeout(TimeSpan.FromMinutes(5));

Registered pipelines with stages

HostLoom.Pipelines.DependencyInjection turns a pipeline into a first-class registration: named stages in declared order, filters resolved transient from a per-run scope so they take repositories and loggers through constructors, and per-filter feature toggles evaluated on every run:

using HostLoom.Pipelines.DependencyInjection;

builder.Services.AddPipeline<IndexingContext>("document-indexing", pipeline => pipeline
    .WithTimeout(TimeSpan.FromMinutes(5))
    .WithRetry(RetryPolicy.Exponential(2, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(30)))
    .Stage("analyze", stage => stage
        .AddFilter<WordCountFilter>(filter => filter.WithName("word_count"))
        .AddFilter<SentenceCountFilter>(filter => filter
            .EnabledWhen(sp => sp.GetRequiredService<IOptionsMonitor<Flags>>().CurrentValue.SentenceCount)))
    .Stage("summarize", stage => stage.AddFilter<ReadingTimeFilter>())
    .Stage("store", stage => stage.AddFilter<StoreDocumentFilter>()));

var runner = provider.GetRequiredKeyedService<IPipelineRunner<IndexingContext>>("document-indexing");
await runner.RunAsync(new IndexingContext(batch, cancellationToken: stoppingToken));

Every pipeline is validated when the host starts — duplicate names and filters with missing constructor dependencies fail startup instead of the first run — and its resolved topology is logged and exposed as runner.Topology. Each filter is automatically wrapped with a duration histogram, a failure counter, and a tracing span (meter and ActivitySource both named HostLoom.Pipelines); the recorded duration is the filter's own work with downstream time subtracted, so the slow filter is visible wherever it sits. WithoutInstrumentation() opts a pipeline out.

HostLoom.Pipelines.Testing completes the loop with CapturePipe (a recording stand-in for next), RecordingFilter, FaultFilter, and harnesses that capture the outcome of a send instead of throwing:

await using var harness = await PipelineHarness.CreateAsync<IndexingContext>("document-indexing", services =>
{
    services.AddSingleton<IDocumentStore>(fakeStore);
    services.AddPipeline<IndexingContext>("document-indexing", Configure);
});

var result = await harness.RunAsync(new IndexingContext(batch));
Assert.True(result.Completed);

Requirements

  • .NET SDK 10.0.400 or later (see global.json)
  • A RabbitMQ or Kafka broker only when those transports are used; HostLoom.Pipelines and the in-memory transport have no external dependencies

Build and verify

dotnet restore HostLoom.slnx
dotnet build HostLoom.slnx -c Release --no-restore
dotnet test HostLoom.slnx -c Release --no-restore

Formatting is enforced with CSharpier, pinned as a local tool:

dotnet tool restore
dotnet csharpier format .

just wraps the same commands; run just to list every recipe:

just format
just build
just test

The suite above is deterministic and in-process. The RabbitMQ and Kafka adapters are additionally exercised against real brokers, which docker-compose.yml provides:

docker compose up -d
dotnet test tests/HostLoom.IntegrationTests/HostLoom.IntegrationTests.csproj -c Release
docker compose down -v

Those tests skip themselves when the broker ports are closed, so the standard gate stays green without Docker — and a skipped result is reported as skipped rather than passed, because without a broker they prove nothing.

Repository map

src/HostLoom/                    messaging kernel
  Abstractions/                  request, handler, behavior, client, broker contracts
  Configuration/                 AddHostLoom builder and DI registration
  Diagnostics/                   the HostLoom ActivitySource
  Exceptions/                    remote fault and request timeout types
  Runtime/                       dispatcher, receive pipeline, executor, client, endpoint
  Serialization/                 System.Text.Json serialization boundary
  Wire/                          envelope, logical type names, codec
src/HostLoom.Analyzers/          Roslyn usage analyzers and rule documentation
src/HostLoom.Diagnostics/        composition ledger, report, and startup reporter
src/HostLoom.Mapping/            dependency-free explicit mapping contracts
src/HostLoom.Mapping.DependencyInjection/ scoped dispatch and closed map registration
src/HostLoom.Mapping.Testing/    container-free mapper composition for tests
src/HostLoom.Pipelines/          transport-neutral middleware pipelines
  Contexts/                      pipe context and thread-safe typed payloads
  Filters/                       delegate, execute, conditional, concurrency, timeout, instrumented, terminal
  Pipes/                         composition, builder, composer
  Diagnostics/                   immutable pipeline probes, pipeline meter and activity source
src/HostLoom.Pipelines.DependencyInjection/ named-stage pipeline registration, runner, startup validation
src/HostLoom.Pipelines.Testing/  capture pipe, recording and fault filters, harnesses
src/HostLoom.Transport.InMemory/ deterministic in-process broker
src/HostLoom.Transport.RabbitMq/ request queues and exclusive reply queues
src/HostLoom.Transport.Kafka/    request/response topics with header correlation
src/HostLoom.AspNetCore.WebSockets/ raw Kestrel WebSocket RPC and subscriptions
benchmarks/HostLoom.Benchmarks/    codec, logging, and mapping benchmarks
examples/HostLoom.Examples.Pipelines/ runnable pipeline tour: DI stages, manual and standalone composition
tests/HostLoom.Tests/            pipeline, round-trip, behavior, and fault tests
tests/HostLoom.IntegrationTests/ RabbitMQ and Kafka transports against real brokers
tests/HostLoom.Analyzers.Tests/  compiler-level analyzer tests

Roadmap toward a Spring-like framework

  1. Harden messaging: delivery policies, retry/dead-letter behaviors, outbox/inbox, trace-context propagation, health checks, broker integration tests, and source-generated contract manifests.
  2. Add starter packages and conditional auto-configuration over Microsoft.Extensions.*.
  3. Add validation, transactions, persistence conventions, HTTP problem details, security, and observability starters.
  4. Add a CLI/template and AOT-safe compile-time registration.

License

HostLoom is licensed under the MIT License.

Copyright (c) 2026 Aleksandr Pavlov.

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on HostLoom.Pipelines.DependencyInjection:

Package Downloads
HostLoom.Pipelines.Testing

Deterministic test doubles and harnesses for HostLoom pipelines.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.17.0 0 10/25/2026
0.16.1 77 10/22/2026
0.16.0 172 10/14/2026
0.15.1 116 10/12/2026
0.15.0 184 10/4/2026
0.14.0 107 10/4/2026
0.13.0 142 9/28/2026
0.12.0 138 9/27/2026
0.11.0 119 9/25/2026
0.10.0 142 9/22/2026
0.9.0 114 9/22/2026
0.8.0 151 9/19/2026
0.7.0 134 9/17/2026
0.6.0 213 9/6/2026
0.5.0 123 9/5/2026
0.4.0 155 9/3/2026
0.3.0 195 8/28/2026
0.2.0 134 8/27/2026
0.1.0 133 8/25/2026