DecoratR 1.4.0
dotnet add package DecoratR --version 1.4.0
NuGet\Install-Package DecoratR -Version 1.4.0
<PackageReference Include="DecoratR" Version="1.4.0" />
<PackageVersion Include="DecoratR" Version="1.4.0" />
<PackageReference Include="DecoratR" />
paket add DecoratR --version 1.4.0
#r "nuget: DecoratR, 1.4.0"
#:package DecoratR@1.4.0
#addin nuget:?package=DecoratR&version=1.4.0
#tool nuget:?package=DecoratR&version=1.4.0
DecoratR
Intuitive .NET library for implementing the Decorator pattern with Microsoft's Dependency Injection container. DecoratR provides a fluent API to chain decorators around your services, enabling cross-cutting concerns like logging, caching, retry logic, and more.
Table of Contents
- Installation
- Quick Start
- Core Concepts
- Basic Usage
- Keyed Services
- Custom Factory Methods
- Generic Decorators
- Lifetime Management
- Best Practices
- API Reference
- Requirements
- Contributing
- License
Installation
dotnet add package DecoratR
Quick Start
using DecoratR;
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
// Basic decoration
services.Decorate<IUserService>()
.With<LoggingDecorator>()
.Then<CacheDecorator>()
.Then<UserService>()
.Apply();
var provider = services.BuildServiceProvider();
var userService = provider.GetService<IUserService>();
// Result: LoggingDecorator -> CacheDecorator -> UserService
Core Concepts
Decorator Chain Order
Decorators are applied in the order they are defined. The last decorator added should be your base implementation:
services.Decorate<IService>()
.With<FirstDecorator>() // Outermost decorator
.Then<SecondDecorator>() // Middle decorator
.Then<BaseService>() // Base implementation (innermost)
.Apply();
Constructor Requirements
Decorators (except the base implementation) must have a constructor that accepts the service type as the first parameter:
public class LoggingDecorator(IService inner) : IService
{
public string Execute() => $"Log({inner.Execute()})";
}
Basic Usage
Simple Decorator Chain
using System.Collections.Concurrent;
// Service interface
public interface IOrderService
{
Task<Order> GetOrderAsync(int orderId);
}
// Logging decorator
public class LoggingDecorator(IOrderService inner) : IOrderService
{
public async Task<Order> GetOrderAsync(int orderId)
{
Console.WriteLine($"Getting order {orderId}");
return await inner.GetOrderAsync(orderId);
}
}
// Cache decorator
public class CacheDecorator(IOrderService inner) : IOrderService
{
private static readonly ConcurrentDictionary<int, Order> _cache = new();
public async Task<Order> GetOrderAsync(int orderId)
{
if (_cache.TryGetValue(orderId, out var cachedOrder))
return cachedOrder;
var order = await inner.GetOrderAsync(orderId);
_cache.TryAdd(orderId, order);
return order;
}
}
// Configuration
services.Decorate<IOrderService>()
.With<LoggingDecorator>()
.Then<CacheDecorator>()
.Then<OrderService>()
.AsScoped()
.Apply();
Conditional Decoration
Apply decorators based on runtime conditions:
services.Decorate<IOrderService>()
.With<LoggingDecorator>()
.ThenIf<RetryDecorator>(env.IsDevelopment()) // Only in development
.ThenIf<CacheDecorator>(enableCaching) // Based on configuration
.Then<OrderService>()
.Apply();
Keyed Services
Note: Keyed services require .NET 8.0 or later. This feature is not available when targeting .NET 6.0 or 7.0.
DecoratR fully supports .NET 8+ keyed services, allowing you to create different decorator chains for the same service type:
// Different configurations for different contexts
services.Decorate<IOrderService>("internal")
.With<LoggingDecorator>()
.Then<OrderService>()
.Apply();
services.Decorate<IOrderService>("external")
.With<LoggingDecorator>()
.Then<RateLimitingDecorator>()
.Then<SecurityDecorator>()
.Then<OrderService>()
.Apply();
services.Decorate<IOrderService>("cached")
.With<CacheDecorator>()
.Then<OrderService>()
.AsSingleton()
.Apply();
// Usage
var internalService = provider.GetRequiredKeyedService<IOrderService>("internal");
var externalService = provider.GetRequiredKeyedService<IOrderService>("external");
var cachedService = provider.GetRequiredKeyedService<IOrderService>("cached");
Complex Keys
You can use any object as a key, including anonymous objects:
var productionKey = new { Environment = "Production", Version = "v2" };
services.Decorate<IOrderService>(productionKey)
.With<AuditDecorator>()
.Then<SecurityDecorator>()
.Then<OrderService>()
.Apply();
// Usage
var service = provider.GetRequiredKeyedService<IOrderService>(productionKey);
Custom Factory Methods
For decorators that require complex initialization or dependencies not easily handled by the DI container:
services.AddSingleton<IMetrics, MetricsService>();
services.AddSingleton<IConfiguration, ConfigurationService>();
services.Decorate<IOrderService>()
.With((serviceProvider, inner) =>
new MetricsDecorator(
inner,
serviceProvider.GetRequiredService<IMetrics>(),
serviceProvider.GetRequiredService<IConfiguration>().GetValue<string>("MetricsPrefix")))
.Then<OrderService>()
.Apply();
Advanced Factory Examples
// Conditional factory with complex logic
services.Decorate<IOrderService>()
.WithIf(enableAdvancedMetrics, (sp, inner) =>
{
var config = sp.GetRequiredService<IConfiguration>();
var metrics = sp.GetRequiredService<IMetrics>();
var logger = sp.GetRequiredService<ILogger<AdvancedMetricsDecorator>>();
return new AdvancedMetricsDecorator(inner, metrics, config, logger);
})
.Then<OrderService>()
.Apply();
// Factory for base implementation
services.Decorate<IOrderService>()
.With<LoggingDecorator>()
.Then((serviceProvider, _) =>
{
var connectionString = serviceProvider.GetRequiredService<IConfiguration>()
.GetConnectionString("DefaultConnection");
return new DatabaseOrderService(connectionString);
})
.Apply();
Generic Decorators
DecoratR supports generic decorators with multiple type parameters:
// Generic decorator with one type parameter
public class CacheDecorator<T>(IService<T> inner) : IService<T>
{
private static readonly Dictionary<string, T> _cache = new();
public async Task<T> GetAsync(string key)
{
if (_cache.TryGetValue(key, out var cachedValue))
return cachedValue;
var value = await inner.GetAsync(key);
_cache[key] = value;
return value;
}
}
// Multiple generic parameters
public class TransformDecorator<TInput, TOutput>(ITransformService<TInput, TOutput> inner)
: ITransformService<TInput, TOutput>
{
public async Task<TOutput> TransformAsync(TInput input)
{
Console.WriteLine($"Transforming {typeof(TInput).Name} to {typeof(TOutput).Name}");
return await inner.TransformAsync(input);
}
}
// Configuration
services.Decorate<IService<string>>()
.With<CacheDecorator<string>>()
.Then<StringService>()
.Apply();
services.Decorate<ITransformService<User, UserDto>>()
.With<TransformDecorator<User, UserDto>>()
.Then<UserTransformService>()
.Apply();
Lifetime Management
Control the lifetime of your decorated services:
// Singleton
services.Decorate<IOrderService>()
.With<CacheDecorator>()
.Then<OrderService>()
.AsSingleton()
.Apply();
// Scoped
services.Decorate<IOrderService>()
.With<LoggingDecorator>()
.Then<OrderService>()
.AsScoped()
.Apply();
// Transient (default)
services.Decorate<IOrderService>()
.With<RetryDecorator>()
.Then<OrderService>()
.AsTransient() // Optional, as it's the default
.Apply();
// Custom lifetime
services.Decorate<IOrderService>()
.With<MetricsDecorator>()
.Then<OrderService>()
.WithLifetime(ServiceLifetime.Scoped)
.Apply();
Best Practices
1. Decorator Ordering
Consider the logical flow of your decorators:
// Good: Logical order from outside to inside
services.Decorate<IService>()
.With<SecurityDecorator>() // Authentication/Authorization first
.Then<RateLimitingDecorator>() // Rate limiting after security
.Then<LoggingDecorator>() // Logging after rate limiting
.Then<MetricsDecorator>() // Metrics collection
.Then<CacheDecorator>() // Cache closest to data source
.Then<BaseService>() // Base implementation
.Apply();
2. Error Handling
Implement proper error handling in decorators:
public class ErrorHandlingDecorator(IService inner, ILogger<ErrorHandlingDecorator> logger) : IService
{
public async Task<Result> ExecuteAsync(Request request)
{
try
{
return await inner.ExecuteAsync(request);
}
catch (Exception ex)
{
logger.LogError(ex, "Error executing request {RequestId}", request.Id);
throw;
}
}
}
3. Performance Considerations
Be mindful of decorator overhead:
// Use conditional decoration for expensive operations
var enableDetailedLogging = config.GetValue<bool>("Logging:Detailed");
services.Decorate<IService>()
.WithIf<DetailedLoggingDecorator>(enableDetailedLogging)
.Then<BaseService>()
.Apply();
4. Testing
Decorators are easy to test in isolation:
[Test]
public void LoggingDecorator_ShouldLogExecution()
{
// Arrange
var mockInner = new Mock<IService>();
var mockLogger = new Mock<ILogger<LoggingDecorator>>();
var decorator = new LoggingDecorator(mockInner.Object, mockLogger.Object);
// Act
decorator.Execute();
// Assert
mockLogger.Verify(x => x.LogInformation(It.IsAny<string>()), Times.Once);
mockInner.Verify(x => x.Execute(), Times.Once);
}
API Reference
Extension Methods
Decorate<TService>()- Begin decoration for a service typeDecorate<TService>(object serviceKey)- Begin decoration for a keyed service
IDecorationBuilder Methods
Then<TDecorator>()- Add a decorator to the chainWith<TDecorator>()- Alias forThen<TDecorator>()Then(Func<IServiceProvider, TService, TService> factory)- Add decorator via factoryThenIf<TDecorator>(bool condition)- Conditionally add decoratorWithIf<TDecorator>(bool condition)- Alias forThenIf<TDecorator>(bool condition)ThenIf(bool condition, Func<IServiceProvider, TService, TService> factory)- Conditionally add decorator via factoryWithLifetime(ServiceLifetime lifetime)- Set service lifetimeAsSingleton()- Set lifetime to SingletonAsScoped()- Set lifetime to ScopedAsTransient()- Set lifetime to TransientApply()- Apply the decoration configuration
Requirements
- .NET 6.0 or later
- Microsoft.Extensions.DependencyInjection 6.0.0 or later
Note: Keyed services are only available in .NET 8.0 or later. If you're using .NET 6.0 or 7.0, you can only use the regular (non-keyed) decoration features.
Contributing
Contributions are welcome! Please feel free to submit issues and pull requests on the GitHub repository.
License
This project is licensed under the MIT License. See the LICENSE file for details.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 is compatible. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. 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
- Microsoft.Extensions.DependencyInjection (>= 10.0.2)
-
net6.0
- Microsoft.Extensions.DependencyInjection (>= 8.0.1)
-
net7.0
- Microsoft.Extensions.DependencyInjection (>= 8.0.1)
-
net8.0
- Microsoft.Extensions.DependencyInjection (>= 8.0.1)
-
net9.0
- Microsoft.Extensions.DependencyInjection (>= 10.0.2)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on DecoratR:
| Package | Downloads |
|---|---|
|
Automation.Playwright
封装的 Playwright 自动化库,便于在应用中通过依赖注入和托管服务使用 Playwright。 |
GitHub repositories
This package is not used by any popular GitHub repositories.