Relay 2.1.0
dotnet add package Relay --version 2.1.0
NuGet\Install-Package Relay -Version 2.1.0
<PackageReference Include="Relay" Version="2.1.0" />
<PackageVersion Include="Relay" Version="2.1.0" />
<PackageReference Include="Relay" />
paket add Relay --version 2.1.0
#r "nuget: Relay, 2.1.0"
#:package Relay@2.1.0
#addin nuget:?package=Relay&version=2.1.0
#tool nuget:?package=Relay&version=2.1.0
π Relay - Adaptive Dependency Injection
Relay your dependency injection to the next level! A powerful, fluent library that extends Microsoft.Extensions.DependencyInjection with adaptive patterns for conditional routing, multi-relays, adapter chains, and dynamic service resolution.
Terms of use
By using this project or its source code, for any purpose and in any shape or form, you grant your implicit agreement to all of the following statements:
- You unequivocally condemn Russia and its military aggression against Ukraine
- You recognize that Russia is an occupant that unlawfully invaded a sovereign state
- You agree that Russia is a terrorist state
- You fully support Ukraine's territorial integrity, including its claims over temporarily occupied territories
- You reject false narratives perpetuated by Russian state propaganda
To learn more about the war and how you can help, click here. Glory to Ukraine! πΊπ¦
π― Why Relay?
Transform the adapter pattern from a simple design pattern into a powerful architectural tool:
- π Conditional Routing: Route to different implementations based on environment, context, or runtime conditions
- π‘ Multi-Relay Broadcasting: Execute operations across multiple implementations with various strategies
- π True Adapter Pattern: Seamlessly integrate legacy systems and incompatible interfaces
- βοΈ Adapter Chains: Build complex transformation pipelines (AβBβCβX)
- π Relay Factories: Key-based service creation with native keyed-DI support
- π‘οΈ Resilience: Failover with per-relay retry, exponential backoff and circuit breaking
- β±οΈ Async Pipelines:
IAsyncAdapterchains for non-blocking I/O transformations - π Observability: Built-in
ActivitySourcetracing andMetermetrics for chains and multi-relays - π©Ί Container Validation: Catch captive dependencies and missing registrations before startup
- π€ Lazy & Scoped Helpers:
Lazy<T>resolution and scope-per-unit-of-work for singletons - π Decorators: Wrap any registration, keyed services included
- β‘ Performance Optimized: Reflection-free adapter chains, trim/AOT compatible, lock-free round-robin
π Quick Start
Installation
dotnet add package Relay
Requirements: .NET 10.0 or later.
Basic Usage
using Relay;
// Configure services
services.AddRelayServices();
// Basic relay
services.AddRelay<IPaymentService, StripePaymentRelay>()
.WithScopedLifetime()
.Build();
// Conditional relay
services.AddConditionalRelay<IPaymentService>()
.When(ctx => ctx.Environment == "Development").RelayTo<MockPaymentService>()
.When(ctx => ctx.Environment == "Production").RelayTo<StripePaymentService>()
.Build();
// Multi-relay broadcasting
services.AddMultiRelay<INotificationService>(config => config
.AddRelay<EmailNotificationService>()
.AddRelay<SmsNotificationService>()
.WithStrategy(RelayStrategy.Broadcast)
).Build();
π Complete Feature Set
1. Basic Relay Registration
services.AddRelay<IPaymentService, StripePaymentRelay>()
.WithScopedLifetime()
.DecorateWith<LoggingDecorator>()
.Build();
2. Conditional Routing
services.AddConditionalRelay<IPaymentService>()
.WithScopedLifetime()
.When(ctx => ctx.Environment == "Development")
.RelayTo<MockPaymentService>()
.When(ctx => ctx.Properties["PaymentMethod"].Equals("Stripe"))
.RelayTo<StripePaymentService>()
.When(ctx => ctx.Properties["PaymentMethod"].Equals("PayPal"))
.RelayTo<PayPalPaymentService>()
.Otherwise<DefaultPaymentService>()
.Build();
3. Multi-Relay Broadcasting
// Broadcast to all services
services.AddMultiRelay<INotificationService>(config => config
.AddRelay<EmailNotificationService>(ServiceLifetime.Singleton)
.AddRelay<SmsNotificationService>(ServiceLifetime.Scoped)
.AddRelay<PushNotificationService>(ServiceLifetime.Transient)
.WithStrategy(RelayStrategy.Broadcast)
).Build();
// Failover strategy with retry (transient-fault resilience). Each provider is retried
// up to maxAttempts with exponential backoff before failing over to the next one.
services.AddMultiRelay<IStorageService>(config => config
.AddRelay<PrimaryStorageService>()
.AddRelay<SecondaryStorageService>()
.AddRelay<BackupStorageService>()
.WithStrategy(RelayStrategy.Failover)
.WithRetry(maxAttempts: 3, delay: TimeSpan.FromMilliseconds(100), backoffFactor: 2.0)
).Build();
4. True Adapter Pattern
// Wrap an incompatible service in an adapter
services.AddAdapter<ITarget, Adaptee>()
.WithScopedLifetime()
.Using<Adapter>();
// Legacy system integration
services.AddAdapter<IModernPaymentService, LegacyPaymentGateway>()
.WithScopedLifetime()
.WithAdapteeLifetime(ServiceLifetime.Singleton)
.Using<LegacyPaymentAdapter>();
// Factory-based adapters
services.AddAdapter<INotificationService, ThirdPartyEmailService>()
.Using(emailService => new EmailNotificationAdapter(emailService));
5. Adapter Chains (AβBβCβX)
// Complex transformation pipeline
services.AddAdapterChain<IResult>()
.From<RawData>() // A (source)
.Then<ValidatedData, ValidationAdapter>() // A β B
.Then<EnrichedData, EnrichmentAdapter>() // B β C
.Finally<ProcessedResultAdapter>() // C β X (final)
.Build();
// Strongly-typed chain
services.AddTypedAdapterChain<XmlData, DomainModel>()
.Then<JsonData, XmlToJsonAdapter>()
.Then<DataDto, JsonToDtoAdapter>()
.Then<DomainModel, DtoToDomainAdapter>()
.Build();
// Named chains β several pipelines producing the same result, picked by name at runtime.
// The source instance is resolved from the container.
services.AddSingleton(new RawData(...));
services.AddAdapterChainFactory<ISettings>()
.AddChain("full")
.From<RawData>().Then<Validated, ValidateAdapter>().Finally<SettingsAdapter>()
.AddChain("fast")
.From<RawData>().Finally<DirectSettingsAdapter>()
.AddChain("mock", _ => new MockSettings()) // or a plain producer delegate
.Build();
var factory = serviceProvider.GetRequiredService<IAdapterChainFactory<ISettings>>();
var settings = factory.CreateFromChain("full");
var names = factory.GetAvailableChains(); // ["full", "fast", "mock"]
6. Relay Factory
// Implementations and their dependencies are created by DI β no manual `new`.
services.AddRelayFactory<IPaymentService>(factory => factory
.RegisterRelay<StripeRelay>("stripe")
.RegisterRelay<PayPalRelay>("paypal")
.RegisterRelay<CryptoRelay>("crypto")
.SetDefaultRelay("stripe")
).Build();
// Usage
var factory = serviceProvider.GetRequiredService<IRelayFactory<IPaymentService>>();
var paymentService = factory.CreateRelay("stripe");
// Escape hatch: the Func<IServiceProvider, T> overload is only for objects that cannot be
// resolved from the container (e.g. third-party SDK clients):
// .RegisterRelay("legacy", sp => new LegacyRelay(sp.GetRequiredService<HttpClient>()))
7. Auto-Discovery
services.AddRelay(config => config
.FromAssemblyOf<IPaymentService>()
.WithDefaultLifetime(ServiceLifetime.Scoped)
.RegisterRelays()
);
// Advanced discovery
services.AddAdaptersFromAssembly<IPaymentService>(ServiceLifetime.Scoped);
8. Comprehensive Lifetime Management
// Different lifetimes for different components
services.AddMultiRelay<INotificationService>(config => config
.WithDefaultLifetime(ServiceLifetime.Scoped)
.AddRelay<EmailService>(ServiceLifetime.Singleton) // Expensive to create
.AddRelay<SmsService>(ServiceLifetime.Scoped) // Request-specific
.AddRelay<PushService>(ServiceLifetime.Transient) // Independent operations
.WithStrategy(RelayStrategy.Broadcast)
).Build();
9. Async Adapter Chains
For transformation pipelines that perform I/O (HTTP, database, file access), use async adapters so steps never block a thread.
public class FetchAdapter : IAsyncAdapter<OrderId, OrderDto>
{
public async Task<OrderDto> AdaptAsync(OrderId id, CancellationToken ct = default)
=> await _api.GetOrderAsync(id, ct);
}
services.AddAsyncAdapterChain<Invoice>()
.From<OrderId>()
.Then<OrderDto, FetchAdapter>() // OrderId β OrderDto (async I/O)
.Then<EnrichedOrder, EnrichAdapter>()
.Finally<InvoiceAdapter>()
.Build();
// Usage
var chain = serviceProvider.GetRequiredService<IAsyncAdapterChain<Invoice>>();
var invoice = await chain.ExecuteAsync(new OrderId(42), cancellationToken);
10. Context-Aware Resolution
IRelayResolver now flows an explicit IRelayContext into conditional relays and factories, so routing decisions can be made per call.
services.AddRelayServices();
services.AddConditionalRelay<IPaymentService>()
.When(ctx => (string)ctx.Properties["tier"] == "premium").RelayTo<PremiumPayment>()
.Otherwise<StandardPayment>()
.Build();
var resolver = scope.ServiceProvider.GetRequiredService<IRelayResolver>();
var ctx = new DefaultRelayContext(scope.ServiceProvider);
ctx.Properties["tier"] = "premium";
var payment = resolver.Resolve<IPaymentService>(ctx); // β PremiumPayment
// Factories can pick a key from the context too
services.AddRelayFactory<IPaymentService>(f => f
.RegisterKeyedRelay<StripeRelay>("stripe")
.RegisterKeyedRelay<PayPalRelay>("paypal")
.SelectKeyByContext(c => (string)c.Properties["provider"])
.SetDefaultRelay("stripe")
).Build();
var svc = factory.CreateRelay(ctx); // key chosen from ctx.Properties["provider"]
11. Native Keyed Services
Register relays against a service key using built-in .NET keyed DI β resolve them with [FromKeyedServices] or GetRequiredKeyedService.
services.AddKeyedRelay<IPaymentService, StripeRelay>("stripe");
services.AddKeyedRelay<IPaymentService, PayPalRelay>("paypal");
// Resolve directly from the container
var stripe = serviceProvider.GetRequiredKeyedService<IPaymentService>("stripe");
// ...or inject by key
public class CheckoutController([FromKeyedServices("paypal")] IPaymentService payment) { }
12. Built-in Observability
Adapter chains and multi-relays emit System.Diagnostics.Activity traces from an ActivitySource named Relay, and metrics from a Meter with the same name. Neither does any work while nothing is listening.
// OpenTelemetry
builder.Services.AddOpenTelemetry()
.WithTracing(t => t.AddSource(RelayDiagnostics.SourceName))
.WithMetrics(m => m.AddMeter(RelayDiagnostics.MeterName));
| Instrument | Kind | Tags |
|---|---|---|
relay.adapter_chain.executions |
Counter | relay.chain.kind, relay.chain.result_type, relay.chain.steps, relay.outcome |
relay.adapter_chain.duration |
Histogram (ms) | same as above |
relay.multi_relay.operations |
Counter | relay.strategy, relay.operation, relay.count, relay.outcome |
relay.multi_relay.duration |
Histogram (ms) | same as above |
relay.circuit_breaker.transitions |
Counter | relay.type, relay.circuit.state |
relay.circuit_breaker.open_circuits |
UpDownCounter | relay.type |
relay.circuit_breaker.skipped |
Counter | relay.type |
That is enough to alert on "any circuit open", chart failover rate by strategy, and watch chain latency without writing a line of instrumentation yourself.
13. Container Validation
Static analysis of your IServiceCollection that runs before the provider is built. It reports
captive dependencies, missing registrations, captured disposable transients and duplicates β all of
them at once, with a message that names the offending type.
using Relay.Validation;
var result = services.ValidateRelayRegistrations();
if (!result.IsValid)
{
logger.LogError("{Issues}", result); // every issue, one per line
}
// ...or fail fast during startup
services.ValidateRelayRegistrationsOrThrow();
// Tune what is checked
services.ValidateRelayRegistrationsOrThrow(new RelayValidationOptions
{
CheckDuplicateRegistrations = true,
IgnoredServiceTypes = [typeof(ILegacyThing)],
});
Sample output:
1 dependency injection issue(s) found:
- [Error] CaptiveDependency: Singleton ReportScheduler (registered as ReportScheduler) captures
scoped IUnitOfWork. The scoped instance would live for the lifetime of the application.
Inject IRelayScopeRunner (or IServiceScopeFactory) and create a scope per unit of work instead.
This complements ValidateOnBuild/ValidateScopes: those only fail when something is resolved,
this one reads the descriptors.
14. Scoped Work From Singletons
The fix for "Cannot consume scoped service from singleton". Registered by AddRelayServices().
public sealed class ReportScheduler(IRelayScopeRunner scopeRunner) // singleton
{
public Task RunAsync(CancellationToken ct) =>
scopeRunner.RunAsync<IUnitOfWork>((unitOfWork, token) => unitOfWork.FlushAsync(token), ct);
}
Run, Run<TService, TResult>, RunAsync and an IServiceProvider overload are available; the
scope (and everything it created) is disposed when the delegate returns.
15. Lazy Resolution
Defer construction of an expensive service, or break a constructor cycle, without a service locator.
services.AddLazyResolution(); // enables Lazy<T> for every registered service
services.AddLazyResolution<IReportRenderer>(); // ...or just one
public sealed class Checkout(Lazy<IReportRenderer> renderer)
{
public string Receipt() => renderer.Value.Render(); // constructed on first use
}
16. Decorators
services.AddScoped<IPaymentService, StripeRelay>();
services.Decorate<IPaymentService, LoggingPaymentDecorator>(); // wraps every registration
services.Decorate<IPaymentService>((inner, sp) => new RetryDecorator(inner)); // factory form
services.TryDecorate<IPaymentService, MetricsDecorator>(); // false instead of throwing
services.DecorateKeyed<IPaymentService, AuditDecorator>("stripe"); // keyed registrations
Decorators stack in the order they are applied and preserve the original lifetime.
17. Circuit Breaker
Retry and failover still pay for a dead relay on every single request. A breaker remembers the outcome: after N consecutive failures the relay is skipped outright, and after the break it gets one trial call before it is trusted again.
services.AddMultiRelay<IPaymentProvider>(config => config
.AddRelay<PrimaryProvider>()
.AddRelay<BackupProvider>()
.WithStrategy(RelayStrategy.Failover)
.WithRetry(maxAttempts: 3, delay: TimeSpan.FromMilliseconds(100))
.WithCircuitBreaker(failureThreshold: 2, breakDuration: TimeSpan.FromSeconds(30)))
.Build();
Retry and breaker compose: the retries run first, and only an exhausted relay counts as one
failure. State transitions are driven entirely by TimeProvider, so a FakeTimeProvider makes
them deterministic in tests, and MultiRelay<T>.GetCircuitSnapshots() exposes the live state for a
health endpoint:
foreach (var circuit in multiRelay.GetCircuitSnapshots())
Console.WriteLine($"{circuit.RelayType.Name}: {circuit.State}");
// PrimaryProvider: Open
// BackupProvider: Closed
Applies to the Failover and FirstSuccessful strategies β the ones where a per-relay failure is
actually observed. Disabled unless you call WithCircuitBreaker.
18. Trimming & Native AOT
The package is marked IsAotCompatible and ships with trim annotations. Adapter chains build
strongly-typed invokers at registration time, so executing a chain performs no reflection at all.
The assembly-scanning entry points (RegisterRelays, ForInterface<T>, AddAdaptersFromAssembly<T>,
and RelayTo(Func<IRelayContext, Type>)) are annotated [RequiresUnreferencedCode] β they still
work in a normal build, and the analyzer tells you if you use them in a trimmed one.
π― Real-World Use Cases
1. Multi-Environment Deployments
- Development: Mock services for fast development
- Staging: Test services with real integrations
- Production: Production services with monitoring
2. Legacy System Modernization
- Gradual migration from old to new systems
- Bridge incompatible interfaces
- Maintain backward compatibility
3. Multi-Provider Integration
- Payment processors (Stripe, PayPal, Square)
- Cloud storage (AWS, Azure, Google Cloud)
- Authentication providers (Auth0, Azure AD, Custom)
4. Data Processing Pipelines
- Raw β Validated β Enriched β Processed
- Multi-step transformations with error handling
- Complex business logic workflows
5. Multi-Channel Broadcasting
- Notifications (Email, SMS, Push, Slack)
- Logging (Console, File, Database, Cloud)
- Caching (Memory, Redis, Database)
ποΈ Architecture Benefits
β Clean Separation of Concerns
- Each adapter/relay has a single responsibility
- Clear interfaces between components
- Easy to test and maintain
β Flexible Configuration
- Runtime decision making
- Environment-specific implementations
- Feature flag support
β Performance Optimized
- Reflection-free adapter chain execution
- Trimming and native AOT compatible
- Proper lifetime management, verified by container validation
β Enterprise Ready
- Thread-safe operations
- Comprehensive error handling
- Built-in distributed tracing via
ActivitySource
π§ͺ Testing Support
// Easy mocking for tests
services.AddConditionalRelay<IPaymentService>()
.When(ctx => ctx.Properties["TestMode"].Equals("Mock"))
.RelayTo<MockPaymentService>()
.When(ctx => ctx.Properties["TestMode"].Equals("Integration"))
.RelayTo<TestPaymentService>()
.Build();
π§ Installation & Setup
Package Installation
# Package Manager
Install-Package Relay
# .NET CLI
dotnet add package Relay
# PackageReference
<PackageReference Include="Relay" Version="2.1.0" />
Basic Setup
using Relay;
public void ConfigureServices(IServiceCollection services)
{
// Add relay services
services.AddRelayServices();
// Configure your relays
services.AddRelay(config => config
.FromAssemblyOf<IPaymentService>()
.RegisterRelays()
);
}
π Documentation
Runnable samples for every feature live in the examples/ directory. Production-oriented
ones worth starting with:
- Resilience β failover across storage providers with retry + exponential backoff
- Observability β tracing an adapter chain via
ActivitySource/ OpenTelemetry - ContextRouting β multi-tenant routing that picks an implementation per request from
IRelayContext - AsyncChain β non-blocking I/O transformation pipeline
- LifetimeSafety β captive dependency detection,
Lazy<T>resolution and scope-per-run - CircuitBreaker β a failing provider tripping its circuit, with live metrics printed from a
MeterListener
The tests/ project doubles as executable documentation of the full API.
π€ Contributing
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
π License
This project is licensed under the MIT License - see the LICENSE file for details.
π Support
If you find this project helpful, please consider:
- β Starring the repository
- π Reporting issues
- π‘ Suggesting new features
- π Improving documentation
Relay transforms dependency injection from simple service registration into a powerful architectural pattern for building maintainable, scalable .NET applications! π
| 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
NuGet packages (2)
Showing the top 2 NuGet packages that depend on Relay:
| Package | Downloads |
|---|---|
|
Log2
Ultra-high-performance .NET 9 structured logger. Lock-free MPSC ring buffer, zero-allocation hot path, SIMD-accelerated layout rendering, 10 built-in outputs with file rotation, circuit breakers, and scoped context. |
|
|
Log2.Sinks.Observability
Log2 sinks for observability backends (Seq, Elasticsearch, OpenTelemetry) over Relay.Sinks.Observability. |
GitHub repositories
This package is not used by any popular GitHub repositories.
See CHANGELOG.md and https://github.com/TarasKovalenko/Relay/releases