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.
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! πΊπ¦
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 and exponential backoff
- β±οΈ Async Pipelines:
IAsyncAdapterchains for non-blocking I/O transformations - π Observability: Built-in
ActivitySourcetracing for chains and multi-relays - β‘ Performance Optimized: Cached reflection and lock-free round-robin resolution
dotnet add package RelayRequirements: .NET 10.0 or later.
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();services.AddRelay<IPaymentService, StripePaymentRelay>()
.WithScopedLifetime()
.DecorateWith<LoggingDecorator>()
.Build();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();// 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();// 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));// 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"]// 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>()))services.AddRelay(config => config
.FromAssemblyOf<IPaymentService>()
.WithDefaultLifetime(ServiceLifetime.Scoped)
.RegisterRelays()
);
// Advanced discovery
services.AddAdaptersFromAssembly<IPaymentService>(ServiceLifetime.Scoped);// 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();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);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"]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) { }Adapter chains and multi-relays emit System.Diagnostics.Activity traces from an ActivitySource named Relay. Activities are only created when a listener is attached, so overhead is zero otherwise.
// OpenTelemetry
builder.Services.AddOpenTelemetry()
.WithTracing(t => t.AddSource(RelayDiagnostics.SourceName));- Development: Mock services for fast development
- Staging: Test services with real integrations
- Production: Production services with monitoring
- Gradual migration from old to new systems
- Bridge incompatible interfaces
- Maintain backward compatibility
- Payment processors (Stripe, PayPal, Square)
- Cloud storage (AWS, Azure, Google Cloud)
- Authentication providers (Auth0, Azure AD, Custom)
- Raw β Validated β Enriched β Processed
- Multi-step transformations with error handling
- Complex business logic workflows
- Notifications (Email, SMS, Push, Slack)
- Logging (Console, File, Database, Cloud)
- Caching (Memory, Redis, Database)
- Each adapter/relay has a single responsibility
- Clear interfaces between components
- Easy to test and maintain
- Runtime decision making
- Environment-specific implementations
- Feature flag support
- Efficient service resolution
- Proper lifetime management
- Minimal overhead
- Thread-safe operations
- Comprehensive error handling
- Built-in distributed tracing via
ActivitySource
// 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();# Package Manager
Install-Package Relay
# .NET CLI
dotnet add package Relay
# PackageReference
<PackageReference Include="Relay" Version="1.2.0" />using Relay;
public void ConfigureServices(IServiceCollection services)
{
// Add relay services
services.AddRelayServices();
// Configure your relays
services.AddRelay(config => config
.FromAssemblyOf<IPaymentService>()
.RegisterRelays()
);
}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
The tests/ project doubles as executable documentation of the full API.
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.
This project is licensed under the MIT License - see the LICENSE file for details.
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! π