Request Tags - PogovorovDaniil/Requestum GitHub Wiki

Request Tags and Routing

This section covers request tagging system in Requestum that enables sophisticated request routing and processing patterns.

📋 Table of Contents


Tagged Requests

Overview

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

How Tagged Requests Work

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.

ITaggedRequest Interface

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; }
}

Key Points

  • 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

HandlerTagAttribute

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; }
}

Usage

// 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
    }
}

Handler Selection Rules

  1. Tagged Request + Tagged Handler: Handler executes if ALL handler tag matches ANY request tag
  2. Tagged Request + No Matching Handler: Falls back to untagged handler (if exists), otherwise throws RequestumException
  3. Untagged Request: Only untagged handlers execute
  4. Multiple Matching Handlers: Throws RequestumException (only for Commands/Queries, not Events)

MiddlewareTagAttribute

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; }
}

Usage

// 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;
        }
    }
}

Middleware Selection Rules

  1. All untagged middlewares always execute
  2. Tagged middlewares execute only if their tag matches ANY request tag
  3. Multiple tagged middlewares can execute for the same request
  4. Execution order: Registration order (same as non-tagged middlewares)

Use Cases

1. Multi-Tenancy

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);
    }
}

2. Environment-Specific Behavior

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);
    }
}

3. Feature Flags

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
    }
}

4. User Role-Based Processing

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);
    }
}

5. Multi-Tag Event Receivers

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

Examples

Complete Multi-Tenancy Example

// 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);
    }
}

Feature Toggle Example

// 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);
    }
}

Best Practices

1. Use Descriptive Tag Names

// ✅ Good - Clear and descriptive
[HandlerTag("premium-tier")]
[HandlerTag("enterprise-tenant")]
[HandlerTag("us-region")]

// ❌ Bad - Vague or cryptic
[HandlerTag("type1")]
[HandlerTag("xyz")]
[HandlerTag("temp")]

2. Document Tag Behavior

/// <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>
{
    // ...
}

3. Provide Fallback Handlers

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) { /* ... */ }
}

4. Keep Tag Logic in One Place

// ✅ 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() };
}

5. Use Constants for Tag Names

// ✅ 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>
{
    // ...
}

6. Test Tag Isolation

[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);
}

Common Pitfalls

Pitfall 1: Forgetting Untagged Fallback

// ❌ 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) { /* ... */ }
}

Pitfall 2: Case Sensitivity

// ❌ 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>
{
    // ...
}

Pitfall 3: Multiple Matching Handlers for Commands/Queries

// ❌ 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> { }

Pitfall 4: Not Understanding Middleware Behavior

// ⚠️ 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);
    }
}

Pitfall 5: Dynamic Tag Arrays

// ⚠️ 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

Overview

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

Configuration

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);
});

How Global Tags Work

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)

Examples

Example 1: Environment-Specific Handlers

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!

Example 2: Global Monitoring Middleware

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<,>));
});

Example 3: Multi-Tenancy with Global Tags

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);
});

Example 4: Multiple Global Tags

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")]

Global Tags vs Request Tags

Global Tags (cfg.GlobalTags)

  • ✅ Applied to ALL requests automatically
  • ✅ Configured once at application startup
  • ✅ Perfect for environment-wide concerns (environment, region, feature flags)
  • ❌ Cannot be changed per-request

Request Tags (ITaggedRequest.Tags)

  • ✅ Applied to specific request instances
  • ✅ Can vary per request
  • ✅ Perfect for request-specific routing (user roles, A/B testing, feature variants)
  • ✅ More granular control

Combining Both Approaches

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)
    }
}

Tag Priority and Selection

When a request has both global tags and request tags:

For Handlers (Commands/Queries):

  1. Handlers matching both global AND request tags (highest priority)
  2. Handlers matching only request tags
  3. Handlers matching only global tags
  4. 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

Best Practices for Global Tags

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 ITaggedRequest instead)
  • Change global tags at runtime (they're set at startup)
  • Mix global and request tag concerns

🎯 Summary

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:

  1. Handlers (Commands/Queries): Only ONE matching handler executes
  2. Event Receivers: ALL matching receivers execute
  3. Middlewares: ALL matching tagged middlewares + ALL untagged middlewares execute
  4. Tag Matching: Matches if ANY handler/middleware tag appears in ANY request tag
  5. Fallback: Untagged handlers serve as fallbacks for commands/queries
  6. Global Tags: Applied to ALL requests automatically at startup
  7. Request Tags: Applied per-request for granular control
  8. 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)

← Back to Home

⚠️ **GitHub.com Fallback** ⚠️