Request Tags - PogovorovDaniil/Requestum GitHub Wiki
This section covers request tagging system in Requestum that enables sophisticated request routing and processing patterns.
Tagged requests provide a powerful mechanism for dynamic handler and middleware selection based on runtime tags. This feature allows you to:
- Route requests to different handlers based on context (e.g., tenant, environment, feature flags)
- Apply conditional middleware based on request characteristics
- Implement multi-tenancy patterns
- Create feature-toggled behavior
- Build environment-specific processing pipelines
The tagging system behaves differently for different types of components:
| Component | Behavior |
|---|---|
| Handlers (Commands & Queries) | Only ONE handler with a matching tag will execute. If no tagged handler matches, falls back to an untagged handler. |
| Event Receivers | ALL receivers with matching tags will execute. Supports multiple tagged receivers. |
| Middlewares | ALL middlewares with matching tags PLUS all untagged middlewares will execute. |
The ITaggedRequest interface marks a request as tagged and provides tag information:
public interface ITaggedRequest
{
/// <summary>
/// Gets the tags associated with this request.
/// Handlers/receivers match if ANY of their tags appear in this array.
/// </summary>
string[] Tags { get; }
}- Requests can have multiple tags
- A handler/middleware matches if ANY of its tags appear in the request's tags
- Tags are case-sensitive strings
- Empty or null tags array behaves like an untagged request
The HandlerTagAttribute marks handlers (command handlers, query handlers, and event receivers) with specific tags:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = true)]
public class HandlerTagAttribute(string tag) : Attribute
{
public string Tag { get; init; }
}// Single tag
[HandlerTag("premium")]
public class PremiumUserCommandHandler : ICommandHandler<CreateUserCommand>
{
public void Execute(CreateUserCommand command)
{
// Premium user logic
}
}
// Multiple tags (handler matches ANY of these tags)
[HandlerTag("admin")]
[HandlerTag("superuser")]
public class AdminQueryHandler : IQueryHandler<GetReportQuery, Report>
{
public Report Handle(GetReportQuery query)
{
// Admin logic
}
}- Tagged Request + Tagged Handler: Handler executes if ALL handler tag matches ANY request tag
-
Tagged Request + No Matching Handler: Falls back to untagged handler (if exists), otherwise throws
RequestumException - Untagged Request: Only untagged handlers execute
-
Multiple Matching Handlers: Throws
RequestumException(only for Commands/Queries, not Events)
The MiddlewareTagAttribute marks middleware with specific tags:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = true)]
public class MiddlewareTagAttribute(string tag) : Attribute
{
public string Tag { get; init; }
}// Tagged middleware
[MiddlewareTag("premium")]
public class PremiumLoggingMiddleware<TRequest, TResponse>
: IAsyncRequestMiddleware<TRequest, TResponse>
{
public async Task<TResponse> InvokeAsync(
TRequest request,
RequestNextAsyncDelegate<TRequest, TResponse> next)
{
// Premium-specific logging
await LogPremiumRequest(request);
return await next.InvokeAsync(request);
}
}
// Untagged middleware (always executes)
public class GlobalExceptionMiddleware<TRequest, TResponse>
: IAsyncRequestMiddleware<TRequest, TResponse>
{
public async Task<TResponse> InvokeAsync(
TRequest request,
RequestNextAsyncDelegate<TRequest, TResponse> next)
{
try
{
return await next.InvokeAsync(request);
}
catch (Exception ex)
{
// Handle exception
throw;
}
}
}- All untagged middlewares always execute
- Tagged middlewares execute only if their tag matches ANY request tag
- Multiple tagged middlewares can execute for the same request
- Execution order: Registration order (same as non-tagged middlewares)
Route requests to tenant-specific handlers and apply tenant-specific middleware:
// Request with tenant tag
public record CreateOrderCommand(string ProductId, int Quantity)
: ICommand<int>, ITaggedRequest
{
public string[] Tags => [TenantContext.CurrentTenantId];
}
// Tenant A handler
[HandlerTag("tenant-a")]
public class TenantAOrderHandler : ICommandHandler<CreateOrderCommand, int>
{
public int Execute(CreateOrderCommand command)
{
// Tenant A specific order processing
return orderId;
}
}
// Tenant B handler
[HandlerTag("tenant-b")]
public class TenantBOrderHandler : ICommandHandler<CreateOrderCommand, int>
{
public int Execute(CreateOrderCommand command)
{
// Tenant B specific order processing
return orderId;
}
}
// Tenant-specific middleware
[MiddlewareTag("tenant-a")]
public class TenantAMiddleware<TRequest, TResponse>
: IAsyncRequestMiddleware<TRequest, TResponse>
{
public async Task<TResponse> InvokeAsync(
TRequest request,
RequestNextAsyncDelegate<TRequest, TResponse> next)
{
// Tenant A specific validation/logging
return await next.InvokeAsync(request);
}
}Different handlers for different environments (dev, staging, production):
public record SendEmailCommand(string To, string Subject, string Body)
: ICommand, ITaggedRequest
{
public string[] Tags => [Environment.GetEnvironmentVariable("ENVIRONMENT") ?? "dev"];
}
// Development: Log emails instead of sending
[HandlerTag("dev")]
public class DevEmailHandler : ICommandHandler<SendEmailCommand>
{
public void Execute(SendEmailCommand command)
{
Console.WriteLine($"[DEV] Email to {command.To}: {command.Subject}");
}
}
// Production: Actually send emails
[HandlerTag("production")]
public class ProductionEmailHandler : ICommandHandler<SendEmailCommand>
{
private readonly IEmailService _emailService;
public void Execute(SendEmailCommand command)
{
_emailService.Send(command.To, command.Subject, command.Body);
}
}Enable/disable features dynamically:
public record ProcessPaymentCommand(decimal Amount, string Method)
: ICommand<string>, ITaggedRequest
{
public string[] Tags { get; init; } = [];
}
// Create command with feature flag
var command = new ProcessPaymentCommand(100m, "stripe")
{
Tags = featureFlags.IsEnabled("new-payment-gateway")
? new[] { "new-gateway" }
: new[] { "legacy-gateway" }
};
// New payment gateway handler
[HandlerTag("new-gateway")]
public class NewPaymentHandler : ICommandHandler<ProcessPaymentCommand, string>
{
public string Execute(ProcessPaymentCommand command)
{
// New payment processing logic
}
}
// Legacy payment gateway handler
[HandlerTag("legacy-gateway")]
public class LegacyPaymentHandler : ICommandHandler<ProcessPaymentCommand, string>
{
public string Execute(ProcessPaymentCommand command)
{
// Legacy payment processing logic
}
}Different handlers for different user roles:
public record GetDashboardQuery(string UserId) : IQuery<Dashboard>, ITaggedRequest
{
public string[] Tags { get; init; } = [];
}
// Create query with user role
var query = new GetDashboardQuery(userId)
{
Tags = new[] { userContext.Role } // "admin", "user", "guest"
};
// Admin dashboard
[HandlerTag("admin")]
public class AdminDashboardHandler : IQueryHandler<GetDashboardQuery, Dashboard>
{
public Dashboard Handle(GetDashboardQuery query)
{
return new Dashboard
{
ShowAdminPanel = true,
ShowAnalytics = true,
ShowAllUsers = true
};
}
}
// Regular user dashboard
[HandlerTag("user")]
public class UserDashboardHandler : IQueryHandler<GetDashboardQuery, Dashboard>
{
public Dashboard Handle(GetDashboardQuery query)
{
return new Dashboard
{
ShowAdminPanel = false,
ShowAnalytics = false,
ShowAllUsers = false
};
}
}
// Role-specific middleware
[MiddlewareTag("admin")]
public class AdminAuditMiddleware<TRequest, TResponse>
: IAsyncRequestMiddleware<TRequest, TResponse>
{
public async Task<TResponse> InvokeAsync(
TRequest request,
RequestNextAsyncDelegate<TRequest, TResponse> next)
{
// Log all admin actions
await _auditLog.LogAsync($"Admin action: {typeof(TRequest).Name}");
return await next.InvokeAsync(request);
}
}Events can have multiple receivers with different tags:
public record UserCreatedEvent(string UserId, string Email)
: IEventMessage, ITaggedRequest
{
public string[] Tags => new[] { "email", "analytics", "audit" };
}
// Email notification receiver
[HandlerTag("email")]
public class EmailNotificationReceiver : IEventMessageReceiver<UserCreatedEvent>
{
public void Receive(UserCreatedEvent message)
{
// Send welcome email
}
}
// Analytics receiver
[HandlerTag("analytics")]
public class AnalyticsReceiver : IEventMessageReceiver<UserCreatedEvent>
{
public void Receive(UserCreatedEvent message)
{
// Track user registration in analytics
}
}
// Audit receiver
[HandlerTag("audit")]
public class AuditReceiver : IEventMessageReceiver<UserCreatedEvent>
{
public void Receive(UserCreatedEvent message)
{
// Log user creation to audit trail
}
}
// All three receivers will execute when the event is published// 1. Define the request
public record CreateInvoiceCommand(string CustomerId, decimal Amount)
: ICommand<int>, ITaggedRequest
{
public string[] Tags { get; init; } = [];
}
// 2. Define tenant-specific handlers
[HandlerTag("acme-corp")]
public class AcmeInvoiceHandler : ICommandHandler<CreateInvoiceCommand, int>
{
private readonly IAcmeInvoiceRepository _repository;
public AcmeInvoiceHandler(IAcmeInvoiceRepository repository)
{
_repository = repository;
}
public int Execute(CreateInvoiceCommand command)
{
// Acme Corp specific invoice creation
// - Uses Acme's custom invoice numbering
// - Applies Acme's tax rules
var invoice = new AcmeInvoice
{
Number = GenerateAcmeInvoiceNumber(),
CustomerId = command.CustomerId,
Amount = command.Amount,
Tax = CalculateAcmeTax(command.Amount)
};
return _repository.Save(invoice);
}
}
[HandlerTag("globex")]
public class GlobexInvoiceHandler : ICommandHandler<CreateInvoiceCommand, int>
{
private readonly IGlobexInvoiceRepository _repository;
public GlobexInvoiceHandler(IGlobexInvoiceRepository repository)
{
_repository = repository;
}
public int Execute(CreateInvoiceCommand command)
{
// Globex specific invoice creation
// - Uses Globex's invoice numbering
// - Applies different tax rules
var invoice = new GlobexInvoice
{
Number = GenerateGlobexInvoiceNumber(),
CustomerId = command.CustomerId,
Amount = command.Amount,
Tax = CalculateGlobexTax(command.Amount)
};
return _repository.Save(invoice);
}
}
// 3. Define tenant-specific middleware
[MiddlewareTag("acme-corp")]
public class AcmeValidationMiddleware<TRequest, TResponse>
: IAsyncRequestMiddleware<TRequest, TResponse>
{
public async Task<TResponse> InvokeAsync(
TRequest request,
RequestNextAsyncDelegate<TRequest, TResponse> next)
{
// Acme-specific validation rules
if (request is CreateInvoiceCommand cmd)
{
if (cmd.Amount > 10000)
throw new ValidationException("Acme Corp: Amount exceeds limit");
}
return await next.InvokeAsync(request);
}
}
[MiddlewareTag("globex")]
public class GlobexValidationMiddleware<TRequest, TResponse>
: IAsyncRequestMiddleware<TRequest, TResponse>
{
public async Task<TResponse> InvokeAsync(
TRequest request,
RequestNextAsyncDelegate<TRequest, TResponse> next)
{
// Globex-specific validation rules
if (request is CreateInvoiceCommand cmd)
{
if (cmd.Amount > 50000)
throw new ValidationException("Globex: Amount exceeds limit");
}
return await next.InvokeAsync(request);
}
}
// 4. Setup DI
services.AddRequestum(cfg =>
{
cfg.RegisterHandlers(typeof(Program).Assembly);
cfg.RegisterMiddleware(typeof(AcmeValidationMiddleware<,>), ServiceLifetime.Scoped);
cfg.RegisterMiddleware(typeof(GlobexValidationMiddleware<,>), ServiceLifetime.Scoped);
});
// 5. Usage - determine tenant and execute
public class InvoiceService
{
private readonly IRequestum _requestum;
private readonly ITenantContext _tenantContext;
public async Task<int> CreateInvoiceAsync(string customerId, decimal amount)
{
var tenantId = _tenantContext.CurrentTenantId; // "acme-corp" or "globex"
var command = new CreateInvoiceCommand(customerId, amount)
{
Tags = new[] { tenantId }
};
// Will execute the correct handler and middleware based on tenant
return await _requestum.ExecuteAsync(command);
}
}// 1. Feature flag service
public interface IFeatureFlags
{
bool IsEnabled(string feature);
}
// 2. Define the query
public record SearchProductsQuery(string SearchTerm)
: IQuery<List<Product>>, ITaggedRequest
{
public string[] Tags { get; init; } = [];
}
// 3. Old search handler
[HandlerTag("old-search")]
public class OldSearchHandler : IQueryHandler<SearchProductsQuery, List<Product>>
{
private readonly IProductRepository _repository;
public List<Product> Handle(SearchProductsQuery query)
{
// Simple LIKE search
return _repository.SearchByName(query.SearchTerm);
}
}
// 4. New ElasticSearch handler
[HandlerTag("new-search")]
public class ElasticSearchHandler : IQueryHandler<SearchProductsQuery, List<Product>>
{
private readonly IElasticClient _elasticClient;
public List<Product> Handle(SearchProductsQuery query)
{
// Advanced full-text search with Elasticsearch
var response = _elasticClient.Search<Product>(s => s
.Query(q => q
.MultiMatch(m => m
.Fields(f => f
.Field(p => p.Name)
.Field(p => p.Description)
)
.Query(query.SearchTerm)
)
)
);
return response.Documents.ToList();
}
}
// 5. Usage with feature flag
public class ProductService
{
private readonly IRequestum _requestum;
private readonly IFeatureFlags _featureFlags;
public List<Product> SearchProducts(string searchTerm)
{
var useNewSearch = _featureFlags.IsEnabled("elasticsearch-search");
var query = new SearchProductsQuery(searchTerm)
{
Tags = useNewSearch
? new[] { "new-search" }
: new[] { "old-search" }
};
return _requestum.Handle<SearchProductsQuery, List<Product>>(query);
}
}// ✅ Good - Clear and descriptive
[HandlerTag("premium-tier")]
[HandlerTag("enterprise-tenant")]
[HandlerTag("us-region")]
// ❌ Bad - Vague or cryptic
[HandlerTag("type1")]
[HandlerTag("xyz")]
[HandlerTag("temp")]/// <summary>
/// Handles order creation for premium customers.
/// Tag: "premium" - Routes premium customer orders
/// Behavior: Applies premium pricing, priority processing
/// </summary>
[HandlerTag("premium")]
public class PremiumOrderHandler : ICommandHandler<CreateOrderCommand>
{
// ...
}Always provide an untagged handler as a fallback for commands/queries:
// Tagged handler for special cases
[HandlerTag("special")]
public class SpecialCaseHandler : ICommandHandler<MyCommand>
{
public void Execute(MyCommand command) { /* ... */ }
}
// Untagged fallback handler
public class DefaultHandler : ICommandHandler<MyCommand>
{
public void Execute(MyCommand command) { /* ... */ }
}// ✅ Good - Centralized tag determination
public class TenantTagProvider
{
public string[] GetCurrentTags() => new[] { _tenantContext.TenantId };
}
public record MyCommand : ICommand, ITaggedRequest
{
private readonly TenantTagProvider _tagProvider;
public MyCommand(TenantTagProvider tagProvider)
{
_tagProvider = tagProvider;
}
public string[] Tags => _tagProvider.GetCurrentTags();
}
// ❌ Bad - Tag logic scattered everywhere
public record MyCommand1 : ICommand, ITaggedRequest
{
public string[] Tags => new[] { GetTenantSomehow() };
}
public record MyCommand2 : ICommand, ITaggedRequest
{
public string[] Tags => new[] { GetDifferentTenant() };
}// ✅ Good - Type-safe constants
public static class Tags
{
public const string Admin = "admin";
public const string User = "user";
public const string Premium = "premium";
public const string TenantA = "tenant-a";
}
[HandlerTag(Tags.Admin)]
public class AdminHandler : ICommandHandler<MyCommand>
{
// ...
}
// ❌ Bad - String literals everywhere (typo-prone)
[HandlerTag("admim")] // Typo!
public class AdminHandler : ICommandHandler<MyCommand>
{
// ...
}[Fact]
public void Execute_WithAdminTag_ExecutesOnlyAdminHandler()
{
// Arrange
var command = new MyCommand { Tags = new[] { Tags.Admin } };
// Act
_requestum.Execute(command);
// Assert
Assert.True(AdminHandler.WasCalled);
Assert.False(UserHandler.WasCalled);
}// ❌ Problem: No untagged handler, request with unknown tag fails
[HandlerTag("premium")]
public class PremiumHandler : ICommandHandler<MyCommand>
{
public void Execute(MyCommand command) { /* ... */ }
}
// Request with "basic" tag throws RequestumException
// ✅ Solution: Add untagged fallback
public class DefaultHandler : ICommandHandler<MyCommand>
{
public void Execute(MyCommand command) { /* ... */ }
}// ❌ Problem: Tags are case-sensitive
[HandlerTag("Admin")] // Capital A
public class AdminHandler : ICommandHandler<MyCommand>
{
// ...
}
var command = new MyCommand { Tags = new[] { "admin" } }; // lowercase a
_requestum.Execute(command); // Won't match!
// ✅ Solution: Use consistent casing (preferably lowercase)
public static class Tags
{
public const string Admin = "admin"; // lowercase
}
[HandlerTag(Tags.Admin)]
public class AdminHandler : ICommandHandler<MyCommand>
{
// ...
}// ❌ Problem: Two handlers match the same tag
[HandlerTag("premium")]
public class PremiumHandler1 : ICommandHandler<MyCommand>
{
// ...
}
[HandlerTag("premium")]
public class PremiumHandler2 : ICommandHandler<MyCommand>
{
// ...
}
var command = new MyCommand { Tags = new[] { "premium" } };
_requestum.Execute(command); // Throws RequestumException!
// ✅ Solution: Use different tags or make one untagged
[HandlerTag("premium-v1")]
public class PremiumHandler1 : ICommandHandler<MyCommand> { }
[HandlerTag("premium-v2")]
public class PremiumHandler2 : ICommandHandler<MyCommand> { }// ⚠️ Remember: Untagged middleware ALWAYS runs
public class GlobalMiddleware<TRequest, TResponse>
: IAsyncRequestMiddleware<TRequest, TResponse>
{
public async Task<TResponse> InvokeAsync(
TRequest request,
RequestNextAsyncDelegate<TRequest, TResponse> next)
{
// This runs for ALL requests, regardless of tags
return await next.InvokeAsync(request);
}
}
// Tagged middleware ADDS to untagged middleware
[MiddlewareTag("premium")]
public class PremiumMiddleware<TRequest, TResponse>
: IAsyncRequestMiddleware<TRequest, TResponse>
{
public async Task<TResponse> InvokeAsync(
TRequest request,
RequestNextAsyncDelegate<TRequest, TResponse> next)
{
// This runs ONLY for premium requests
// BUT GlobalMiddleware also runs!
return await next.InvokeAsync(request);
}
}// ⚠️ Careful: Tags are evaluated once when request is created
public record MyCommand : ICommand, ITaggedRequest
{
// ❌ Problem: This is evaluated every time Tags is accessed
public string[] Tags => new[] { DateTime.Now.Hour > 12 ? "pm" : "am" };
}
// ✅ Solution: Evaluate tags once at creation
public record MyCommand : ICommand, ITaggedRequest
{
public MyCommand()
{
Tags = new[] { DateTime.Now.Hour > 12 ? "pm" : "am" };
}
public string[] Tags { get; init; }
}Global Tags provide a powerful way to apply tags to ALL requests processed through Requestum without requiring each request to implement ITaggedRequest. This feature enables:
- Environment-specific routing (production vs development handlers)
- Cross-cutting concerns (monitoring, logging, auditing)
- Multi-tenancy at the application level
- Regional deployment strategies
- Feature flags at infrastructure level
Global tags are configured during Requestum service registration:
services.AddRequestum(cfg =>
{
// Apply global tags to all requests
cfg.GlobalTags = ["production", "monitoring"];
cfg.RegisterHandlers(typeof(Program).Assembly);
cfg.RegisterMiddlewares(typeof(Program).Assembly);
});When GlobalTags are configured, Requestum automatically applies these tags to every request as if they implemented ITaggedRequest:
| Component | Behavior with Global Tags |
|---|---|
| Handlers (Commands & Queries) | Requestum prefers handlers with matching global tags over untagged handlers |
| Event Receivers | ALL receivers with matching global tags will execute |
| Middlewares | ALL middlewares with matching global tags execute (in addition to untagged middlewares) |
Different payment processing based on environment:
// Production payment handler - real payment processing
[HandlerTag("production")]
public class ProductionPaymentHandler : IAsyncCommandHandler<ProcessPaymentCommand>
{
private readonly IStripeService _stripe;
public ProductionPaymentHandler(IStripeService stripe)
{
_stripe = stripe;
}
public async Task ExecuteAsync(ProcessPaymentCommand command, CancellationToken ct = default)
{
// Real payment processing with Stripe
await _stripe.ChargeAsync(command.Amount, command.CardToken, ct);
}
}
// Development mock payment handler
[HandlerTag("development")]
public class MockPaymentHandler : IAsyncCommandHandler<ProcessPaymentCommand>
{
private readonly ILogger _logger;
public MockPaymentHandler(ILogger logger)
{
_logger = logger;
}
public async Task ExecuteAsync(ProcessPaymentCommand command, CancellationToken ct = default)
{
// Mock payment processing for development
_logger.LogInformation($"Mock payment: ${command.Amount}");
await Task.CompletedTask;
}
}
// Configuration based on environment
var environment = builder.Environment.EnvironmentName.ToLowerInvariant();
services.AddRequestum(cfg =>
{
cfg.GlobalTags = [environment]; // "production" or "development"
cfg.RegisterHandlers(typeof(Program).Assembly);
});Now all payment commands will automatically use the appropriate handler based on the environment!
Apply performance monitoring to all requests:
[MiddlewareTag("monitoring")]
public class PerformanceMonitoringMiddleware<TRequest, TResponse>
: IAsyncRequestMiddleware<TRequest, TResponse>
{
private readonly IMetricsService _metrics;
public PerformanceMonitoringMiddleware(IMetricsService metrics)
{
_metrics = metrics;
}
public async Task<TResponse> InvokeAsync(
TRequest request,
AsyncRequestNextDelegate<TRequest, TResponse> next,
CancellationToken cancellationToken = default)
{
var stopwatch = Stopwatch.StartNew();
try
{
var response = await next.InvokeAsync(request);
stopwatch.Stop();
await _metrics.RecordExecutionTimeAsync(
typeof(TRequest).Name,
stopwatch.ElapsedMilliseconds
);
return response;
}
catch (Exception ex)
{
stopwatch.Stop();
await _metrics.RecordFailureAsync(typeof(TRequest).Name, ex);
throw;
}
}
}
// Enable monitoring for all requests
services.AddRequestum(cfg =>
{
cfg.GlobalTags = ["monitoring"];
cfg.RegisterHandlers(typeof(Program).Assembly);
cfg.RegisterMiddleware(typeof(PerformanceMonitoringMiddleware<,>));
});Route all requests to tenant-specific handlers:
// Tenant-specific event receivers
[HandlerTag("tenant-a")]
public class TenantANotificationReceiver : IAsyncEventMessageReceiver<OrderCreatedEvent>
{
public async Task ReceiveAsync(OrderCreatedEvent message, CancellationToken ct = default)
{
// Send notification using Tenant A's configuration
await SendToTenantAChannelAsync(message);
}
}
[HandlerTag("tenant-b")]
public class TenantBNotificationReceiver : IAsyncEventMessageReceiver<OrderCreatedEvent>
{
public async Task ReceiveAsync(OrderCreatedEvent message, CancellationToken ct = default)
{
// Send notification using Tenant B's configuration
await SendToTenantBChannelAsync(message);
}
}
// Configure based on current tenant context
var tenantId = GetCurrentTenantId(); // From configuration or context
services.AddRequestum(cfg =>
{
cfg.GlobalTags = [$"tenant-{tenantId}"];
cfg.RegisterHandlers(typeof(Program).Assembly);
});Combine multiple global tags for complex scenarios:
var environment = builder.Environment.EnvironmentName.ToLowerInvariant();
var region = configuration["DeploymentRegion"]; // "us-east", "eu-west", etc.
services.AddRequestum(cfg =>
{
cfg.GlobalTags =
[
environment, // "production" or "development"
"monitoring", // Enable performance tracking
"logging", // Enable detailed logging
$"region-{region}" // "region-us-east", "region-eu-west", etc.
];
cfg.RegisterHandlers(typeof(Program).Assembly);
cfg.RegisterMiddlewares(typeof(Program).Assembly);
});
// Now all these tagged handlers/middlewares will be active:
// - [HandlerTag("production")] or [HandlerTag("development")]
// - [MiddlewareTag("monitoring")]
// - [MiddlewareTag("logging")]
// - [HandlerTag("region-us-east")] or [HandlerTag("region-eu-west")]- ✅ Applied to ALL requests automatically
- ✅ Configured once at application startup
- ✅ Perfect for environment-wide concerns (environment, region, feature flags)
- ❌ Cannot be changed per-request
- ✅ Applied to specific request instances
- ✅ Can vary per request
- ✅ Perfect for request-specific routing (user roles, A/B testing, feature variants)
- ✅ More granular control
You can use both global and request tags together:
// Global tags for environment-wide concerns
services.AddRequestum(cfg =>
{
cfg.GlobalTags = ["production", "monitoring"];
cfg.RegisterHandlers(typeof(Program).Assembly);
});
// Request-specific tags for per-request routing
public record ProcessOrderCommand : ICommand, ITaggedRequest
{
public int OrderId { get; set; }
// Request-level tags (e.g., for A/B testing)
public string[] Tags => ["new-checkout-flow"];
}
// This handler matches ONLY production environment with new checkout flow
[HandlerTag("production")]
[HandlerTag("new-checkout-flow")]
public class NewProductionCheckoutHandler : ICommandHandler<ProcessOrderCommand>
{
public void Execute(ProcessOrderCommand command)
{
// New checkout logic for production
}
}
// This handler matches ONLY development with new checkout flow
[HandlerTag("development")]
[HandlerTag("new-checkout-flow")]
public class NewDevelopmentCheckoutHandler : ICommandHandler<ProcessOrderCommand>
{
public void Execute(ProcessOrderCommand command)
{
// New checkout logic for development (with mocks)
}
}When a request has both global tags and request tags:
For Handlers (Commands/Queries):
- Handlers matching both global AND request tags (highest priority)
- Handlers matching only request tags
- Handlers matching only global tags
- Untagged handlers (fallback)
For Middlewares: ALL matching middlewares execute:
- Middlewares matching global tags
- Middlewares matching request tags
- All untagged middlewares
For Event Receivers: ALL matching receivers execute:
- Receivers matching global tags
- Receivers matching request tags
- All untagged receivers
✅ DO:
- Use global tags for infrastructure concerns (environment, region)
- Keep global tag names lowercase and consistent
- Document global tags in your configuration
- Use constants for tag names to avoid typos
public static class GlobalTagNames
{
public const string Production = "production";
public const string Development = "development";
public const string Monitoring = "monitoring";
public const string RegionUsEast = "region-us-east";
}
services.AddRequestum(cfg =>
{
cfg.GlobalTags = [GlobalTagNames.Production, GlobalTagNames.Monitoring];
// ...
});❌ DON'T:
- Use global tags for request-specific logic (use
ITaggedRequestinstead) - Change global tags at runtime (they're set at startup)
- Mix global and request tag concerns
Tagged requests and Global Tags provide powerful mechanisms for:
- ✅ Dynamic handler selection based on runtime context
- ✅ Conditional middleware execution for specific scenarios
- ✅ Multi-tenancy with tenant-specific logic
- ✅ Feature toggles for gradual rollouts
- ✅ Environment-specific behavior (dev/staging/production)
- ✅ Role-based processing for different user types
- ✅ Infrastructure-level routing with Global Tags
- ✅ Cross-cutting concerns (monitoring, logging, auditing)
Key Rules to Remember:
- Handlers (Commands/Queries): Only ONE matching handler executes
- Event Receivers: ALL matching receivers execute
- Middlewares: ALL matching tagged middlewares + ALL untagged middlewares execute
- Tag Matching: Matches if ANY handler/middleware tag appears in ANY request tag
- Fallback: Untagged handlers serve as fallbacks for commands/queries
- Global Tags: Applied to ALL requests automatically at startup
- Request Tags: Applied per-request for granular control
- Combined Tags: You can use both global and request tags together
Quick Decision Guide:
| Use Case | Solution |
|---|---|
| Environment-specific handlers | Global Tags |
| Tenant-specific routing (app-wide) | Global Tags |
| User role-based handlers | Request Tags (ITaggedRequest) |
| A/B testing variants | Request Tags (ITaggedRequest) |
| Monitoring/logging all requests | Global Tags with tagged middleware |
| Per-request authorization | Request Tags (ITaggedRequest) |