Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 23 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Transform the adapter pattern from a simple design pattern into a **powerful arc
- **🔗 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**: `IAsyncAdapter` chains for non-blocking I/O transformations
- **🔭 Observability**: Built-in `ActivitySource` tracing for chains and multi-relays
- **⚡ Performance Optimized**: Cached reflection and lock-free round-robin resolution
Expand Down Expand Up @@ -102,12 +103,14 @@ services.AddMultiRelay<INotificationService>(config => config
.WithStrategy(RelayStrategy.Broadcast)
).Build();

// Failover strategy
// 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();
```

Expand Down Expand Up @@ -164,16 +167,21 @@ var names = factory.GetAvailableChains(); // ["full", "fast", "mock"]

### **6. Relay Factory**
```csharp
// Implementations and their dependencies are created by DI — no manual `new`.
services.AddRelayFactory<IPaymentService>(factory => factory
.RegisterRelay("stripe", provider => new StripeRelay())
.RegisterRelay("paypal", provider => new PayPalRelay())
.RegisterRelay("crypto", provider => new CryptoRelay())
.RegisterRelay<StripeRelay>("stripe")
.RegisterRelay<PayPalRelay>("paypal")
.RegisterRelay<CryptoRelay>("crypto")
.SetDefaultRelay("stripe")
).Build();

// Usage
var factory = serviceProvider.GetService<IRelayFactory<IPaymentService>>();
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**
Expand Down Expand Up @@ -338,7 +346,7 @@ Install-Package Relay
dotnet add package Relay

# PackageReference
<PackageReference Include="Relay" Version="1.1.0" />
<PackageReference Include="Relay" Version="1.2.0" />
```

### **Basic Setup**
Expand All @@ -360,9 +368,15 @@ public void ConfigureServices(IServiceCollection services)

## 📚 **Documentation**

Runnable samples for every feature live in the [`examples/`](examples) directory — including
basic relays, conditional routing, multi-relay strategies, adapter chains, async chains, and
factories. The [`tests/`](tests) project doubles as executable documentation of the full API.
Runnable samples for every feature live in the [`examples/`](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/`](tests) project doubles as executable documentation of the full API.

## 🤝 **Contributing**

Expand Down
264 changes: 0 additions & 264 deletions Relay.sln

This file was deleted.

32 changes: 32 additions & 0 deletions Relay.slnx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<Solution>
<Configurations>
<Platform Name="Any CPU" />
<Platform Name="x64" />
<Platform Name="x86" />
</Configurations>
<Folder Name="/Examples/">
<Project Path="examples/Relay.Examples.Adapter/Relay.Examples.Adapter.csproj" />
<Project Path="examples/Relay.Examples.AdapterChain/Relay.Examples.AdapterChain.csproj" />
<Project Path="examples/Relay.Examples.AdapterFactory/Relay.Examples.AdapterFactory.csproj" />
<Project Path="examples/Relay.Examples.AdvancedDecorator/Relay.Examples.AdvancedDecorator.csproj" />
<Project Path="examples/Relay.Examples.AsyncChain/Relay.Examples.AsyncChain.csproj" />
<Project Path="examples/Relay.Examples.Basic/Relay.Examples.Basic.csproj" />
<Project Path="examples/Relay.Examples.Conditional/Relay.Examples.Conditional.csproj" />
<Project Path="examples/Relay.Examples.ConditionalRouting/Relay.Examples.ConditionalRouting.csproj" />
<Project Path="examples/Relay.Examples.ContextRouting/Relay.Examples.ContextRouting.csproj" />
<Project Path="examples/Relay.Examples.DecorateWith/Relay.Examples.DecorateWith.csproj" />
<Project Path="examples/Relay.Examples.Factory/Relay.Examples.Factory.csproj" />
<Project Path="examples/Relay.Examples.Integration/Relay.Examples.Integration.csproj" />
<Project Path="examples/Relay.Examples.MultiBroadcasting/Relay.Examples.MultiBroadcasting.csproj" />
<Project Path="examples/Relay.Examples.MultiFailover/Relay.Examples.MultiFailover.csproj" />
<Project Path="examples/Relay.Examples.Observability/Relay.Examples.Observability.csproj" />
<Project Path="examples/Relay.Examples.ParallelWithResults/Relay.Examples.ParallelWithResults.csproj" />
<Project Path="examples/Relay.Examples.Resilience/Relay.Examples.Resilience.csproj" />
</Folder>
<Folder Name="/src/">
<Project Path="src/Relay.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/Relay.Tests/Relay.Tests.csproj" />
</Folder>
</Solution>
56 changes: 56 additions & 0 deletions examples/Relay.Examples.ContextRouting/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using Microsoft.Extensions.DependencyInjection;
using Relay;
using Relay.Core.Implementations;
using Relay.Core.Interfaces;
using Relay.Examples.ContextRouting;

// Production scenario: multi-tenant routing. The same IFeatureService resolves to a different
// implementation per request based on the tenant's plan, decided at call time via IRelayContext.

var services = new ServiceCollection();
services.AddRelayServices();
services
.AddConditionalRelay<IFeatureService>()
.When(ctx => (string)ctx.Properties["plan"] == "enterprise")
.RelayTo<EnterpriseFeatures>()
.When(ctx => (string)ctx.Properties["plan"] == "pro")
.RelayTo<ProFeatures>()
.Otherwise<FreeFeatures>()
.Build();

var provider = services.BuildServiceProvider();

foreach (var plan in new[] { "free", "pro", "enterprise" })
{
using var scope = provider.CreateScope();
var resolver = scope.ServiceProvider.GetRequiredService<IRelayResolver>();

var ctx = new DefaultRelayContext(scope.ServiceProvider);
ctx.Properties["plan"] = plan;

var service = resolver.Resolve<IFeatureService>(ctx);
Console.WriteLine($"plan={plan,-10} -> {service.Describe()}");
}

namespace Relay.Examples.ContextRouting
{
public interface IFeatureService
{
string Describe();
}

public sealed class FreeFeatures : IFeatureService
{
public string Describe() => "Free: 1 seat, community support";
}

public sealed class ProFeatures : IFeatureService
{
public string Describe() => "Pro: 10 seats, email support";
}

public sealed class EnterpriseFeatures : IFeatureService
{
public string Describe() => "Enterprise: unlimited seats, SSO, 24/7 support";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Relay.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
</ItemGroup>
</Project>
61 changes: 61 additions & 0 deletions examples/Relay.Examples.Observability/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using System.Diagnostics;
using Microsoft.Extensions.DependencyInjection;
using Relay;
using Relay.Core.Interfaces;
using Relay.Diagnostics;
using Relay.Examples.Observability;

// Production scenario: trace an adapter chain end to end. In a real app you'd wire
// RelayDiagnostics.SourceName into OpenTelemetry; here we attach a plain ActivityListener
// and print each span so you can see the chain's steps.

using var listener = new ActivityListener
{
ShouldListenTo = source => source.Name == RelayDiagnostics.SourceName,
Sample = (ref ActivityCreationOptions<ActivityContext> _) => ActivitySamplingResult.AllData,
ActivityStarted = a => Console.WriteLine($" -> start {a.OperationName}"),
ActivityStopped = a =>
Console.WriteLine(
$" <- stop {a.OperationName} ({a.Duration.TotalMilliseconds:F1} ms) "
+ string.Join(" ", a.Tags.Select(t => $"{t.Key}={t.Value}"))
),
};
ActivitySource.AddActivityListener(listener);

var services = new ServiceCollection();
services
.AddAdapterChain<Receipt>()
.From<Order>()
.Then<PricedOrder, PricingAdapter>()
.Finally<ReceiptAdapter>()
.Build();

var provider = services.BuildServiceProvider();
using var scope = provider.CreateScope();
var chain = scope.ServiceProvider.GetRequiredService<IAdapterChain<Receipt>>();

Console.WriteLine("Executing traced chain:");
var receipt = chain.Execute(new Order("SKU-1", 3));
Console.WriteLine($"Result: {receipt}");

namespace Relay.Examples.Observability
{
public record Order(string Sku, int Quantity);

public record PricedOrder(string Sku, decimal Total);

public record Receipt(string Sku, string Total)
{
public override string ToString() => $"{Sku} = {Total}";
}

public sealed class PricingAdapter : IAdapter<Order, PricedOrder>
{
public PricedOrder Adapt(Order source) => new(source.Sku, source.Quantity * 9.99m);
}

public sealed class ReceiptAdapter : IAdapter<PricedOrder, Receipt>
{
public Receipt Adapt(PricedOrder source) => new(source.Sku, $"{source.Total:C}");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Relay.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
</ItemGroup>
</Project>
65 changes: 65 additions & 0 deletions examples/Relay.Examples.Resilience/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using Microsoft.Extensions.DependencyInjection;
using Relay;
using Relay.Core.Enums;
using Relay.Core.Interfaces;
using Relay.Examples.Resilience;

// Production scenario: write to a primary storage provider, fall back to a secondary, and
// retry each provider for transient faults before failing over. Mirrors how you'd harden a
// multi-region write path.

var services = new ServiceCollection();
services
.AddMultiRelay<IStorageProvider>(config =>
config
.AddRelay<PrimaryStorage>()
.AddRelay<SecondaryStorage>()
.WithStrategy(RelayStrategy.Failover)
.WithRetry(maxAttempts: 3, delay: TimeSpan.FromMilliseconds(50), backoffFactor: 2.0)
)
.Build();

var provider = services.BuildServiceProvider();
using var scope = provider.CreateScope();
var storage = scope.ServiceProvider.GetRequiredService<IMultiRelay<IStorageProvider>>();

// Primary fails its first 2 attempts (transient), succeeds on the 3rd — retry recovers it,
// no failover needed.
var result = await storage.RelayToAllWithResults(s => s.SaveAsync("order-42"));
Console.WriteLine($"Saved via: {string.Join(", ", result)}");

namespace Relay.Examples.Resilience
{
public interface IStorageProvider
{
Task<string> SaveAsync(string payload);
}

// Fails the first two attempts to simulate a transient outage, then succeeds.
public sealed class PrimaryStorage : IStorageProvider
{
private int _attempts;

public async Task<string> SaveAsync(string payload)
{
_attempts++;
await Task.Delay(10);
if (_attempts < 3)
{
Console.WriteLine($" [primary] attempt {_attempts} failed (transient)");
throw new TimeoutException("primary temporarily unavailable");
}
Console.WriteLine($" [primary] attempt {_attempts} succeeded");
return $"primary:{payload}";
}
}

public sealed class SecondaryStorage : IStorageProvider
{
public async Task<string> SaveAsync(string payload)
{
await Task.Delay(10);
return $"secondary:{payload}";
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Relay.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
</ItemGroup>
</Project>
8 changes: 6 additions & 2 deletions src/Builders/ConditionalRelayBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,12 @@ public IServiceCollection Build()
}
var implementationType =
registration.ImplementationType ?? registration.TypeSelector!(context);
return (TInterface)
ActivatorUtilities.CreateInstance(provider, implementationType);
// Reuse an existing DI registration (and its lifetime) when the implementation
// is registered; otherwise create it with its constructor dependencies resolved.
var instance =
provider.GetService(implementationType)
?? ActivatorUtilities.CreateInstance(provider, implementationType);
return (TInterface)instance;
},
_lifetime
)
Expand Down
Loading
Loading