Skip to content

Scenarios

A. Shafie edited this page Apr 22, 2025 · 2 revisions

Scenarios

Contextual Scenario Handling

In real-world applications, the same message (command, query, or event) often needs to be processed differently depending on the context in which it's invoked. LiteBus provides two mechanisms to handle these contextual scenarios: tag-based filtering and predicate-based filtering.

Understanding Contextual Processing

Contextual processing is valuable when the same logical operation needs different handling based on:

  • The source of the request (public API, internal service, admin interface)
  • The operational environment (production, staging, testing)
  • User roles or permissions (admin, regular user, system)
  • Business workflows (standard flow, expedited flow)
  • Processing priorities (high priority, normal, background)

Without contextual handling, you would need to create separate commands/queries for each context or implement complex conditional logic within handlers.

1. Handler Tagging System

LiteBus provides two attributes for implementing contextual handling:

HandlerTag Attribute

Applied to handlers to specify that they should only execute in specific contexts:

[HandlerTag("PublicAPI")]
public class StrictValidationHandler : ICommandPreHandler<UpdateUserCommand>
{
    public Task PreHandleAsync(UpdateUserCommand command, CancellationToken cancellationToken = default)
    {
        // Apply strict validation rules for requests from public API
        // ...
    }
}

HandlerTags Attribute

Applied to handlers that should execute in multiple contexts:

[HandlerTags("Admin", "Internal")]
public class ExtendedDataHandler : IQueryHandler<GetUserQuery, UserDetailsDto>
{
    public Task<UserDetailsDto> HandleAsync(GetUserQuery query, CancellationToken cancellationToken = default)
    {
        // Include sensitive information only for admin and internal requests
        // ...
    }
}

2. Predicate-Based Handler Filtering

Beyond static tags, LiteBus also supports dynamic filtering of handlers using predicates. This is particularly useful for:

  • Runtime filtering based on complex conditions
  • Filtering handlers by implementing marker interfaces
  • Situations where tags would be too static or verbose

Event Handlers Filtering

// Define a marker interface for certain handlers
public interface IHighPriorityHandler { }

// Implement the marker interface on selected handlers
public class CriticalSystemAlertHandler : IEventHandler<SystemAlertEvent>, IHighPriorityHandler
{
    public Task HandleAsync(SystemAlertEvent @event, CancellationToken cancellationToken = default)
    {
        // Handle critical system alerts
    }
}

// Regular handler without the marker
public class StandardSystemAlertHandler : IEventHandler<SystemAlertEvent>
{
    public Task HandleAsync(SystemAlertEvent @event, CancellationToken cancellationToken = default)
    {
        // Handle normal system alerts
    }
}

// Filter handlers at runtime using a predicate
await _eventMediator.PublishAsync(new SystemAlertEvent 
{
    AlertLevel = AlertLevel.Critical
}, 
new EventMediationSettings
{
    Filters = 
    {
        // Only execute handlers that implement IHighPriorityHandler
        HandlerPredicate = type => type.IsAssignableTo(typeof(IHighPriorityHandler))
    }
});

Dynamic Filtering Based on Runtime Conditions

// Use a factory to create appropriate filter settings
private EventMediationSettings CreateFilterSettings(SystemAlertEvent @event)
{
    return new EventMediationSettings
    {
        Filters = 
        {
            // Apply different predicates based on event properties
            HandlerPredicate = @event.AlertLevel switch
            {
                AlertLevel.Critical => type => type.IsAssignableTo(typeof(IHighPriorityHandler)),
                AlertLevel.Warning => type => !type.Name.Contains("Diagnostic"),
                AlertLevel.Info => type => type.Name.Contains("Logger") || type.Name.Contains("Audit"),
                _ => _ => true // Default to all handlers
            }
        }
    };
}

// Use the dynamic settings
await _eventMediator.PublishAsync(alertEvent, CreateFilterSettings(alertEvent));

Tag Selection Rules

When messages are mediated with specific tags:

  1. Handlers with no tags are always executed (they're considered universal)
  2. Handlers with at least one matching tag are executed
  3. Handlers with tags but no matching tags are skipped

This provides a flexible and intuitive filtering mechanism.

Implementation Examples

Public API vs Internal Validation

Different validation rules based on request source:

// For public API requests
[HandlerTag("PublicAPI")]
public class ExternalApiValidator : ICommandValidator<CreateUserCommand>
{
    public Task ValidateAsync(CreateUserCommand command, CancellationToken cancellationToken = default)
    {
        // More stringent validation for external requests
        if (string.IsNullOrEmpty(command.Email))
            throw new ValidationException("Email is required for public API requests");
            
        // Check for valid email format
        if (!IsValidEmailFormat(command.Email))
            throw new ValidationException("Invalid email format");
            
        return Task.CompletedTask;
    }
}

// For internal requests
[HandlerTag("Internal")]
public class InternalSystemValidator : ICommandValidator<CreateUserCommand>
{
    public Task ValidateAsync(CreateUserCommand command, CancellationToken cancellationToken = default)
    {
        // Basic validation for internal use
        if (string.IsNullOrEmpty(command.Username))
            throw new ValidationException("Username is required");
            
        return Task.CompletedTask;
    }
}

// Common validation that applies to all contexts (no tag)
public class CommonUserValidator : ICommandValidator<CreateUserCommand>
{
    public Task ValidateAsync(CreateUserCommand command, CancellationToken cancellationToken = default)
    {
        // Common validation rules for all contexts
        if (command.Username?.Length < 3)
            throw new ValidationException("Username must be at least 3 characters");
            
        return Task.CompletedTask;
    }
}

Context-Specific Data Enrichment

Different event processing based on recipient systems:

[HandlerTag("DataWarehouse")]
public class DataWarehouseEnrichmentHandler : IEventHandler<OrderPlacedEvent>
{
    public Task HandleAsync(OrderPlacedEvent @event, CancellationToken cancellationToken = default)
    {
        // Enrich and transform event data for data warehouse consumption
        // Add analytics-specific information
        // ...
    }
}

[HandlerTag("CustomerNotification")]
public class CustomerNotificationHandler : IEventHandler<OrderPlacedEvent>
{
    public Task HandleAsync(OrderPlacedEvent @event, CancellationToken cancellationToken = default)
    {
        // Process event for customer notification purposes
        // Include only customer-relevant information
        // ...
    }
}

Combining Tags and Predicate Filtering

For complex scenarios, combine both approaches:

// Define marker interfaces for handler capabilities
public interface IHighLatencyTolerant { } 
public interface IErrorResilient { }

// Apply both tags and implement marker interfaces
[HandlerTag("ReportingSystem")]
public class ReportingEventHandler : IEventHandler<DataChangedEvent>, IHighLatencyTolerant
{
    // Handler implementation
}

// Create specialized filters
public static class HandlerFilters
{
    public static EventMediationSettings CreateReportingSettings(bool isHighLoad)
    {
        return new EventMediationSettings
        {
            Filters = 
            {
                // Apply tag filter
                Tags = ["ReportingSystem"],
                
                // Additionally filter by load condition
                HandlerPredicate = isHighLoad 
                    ? type => type.IsAssignableTo(typeof(IHighLatencyTolerant))
                    : _ => true // Under normal load, include all tagged handlers
            }
        };
    }
}

// Usage
var isHighLoad = _systemMonitor.IsUnderHighLoad();
await _eventMediator.PublishAsync(dataEvent, HandlerFilters.CreateReportingSettings(isHighLoad));

Specifying Context During Mediation

When sending commands, executing queries, or publishing events, you specify the context:

Using Extension Methods

Simple approach using extension methods with a single tag:

// Command with context
await _commandMediator.SendAsync(new UpdateUserCommand
{
    UserId = userId,
    Email = email
}, 
"PublicAPI", // Context tag
cancellationToken);

// Query with context
var product = await _queryMediator.QueryAsync(
    new GetProductQuery { ProductId = productId },
    "MobileApp", // Context tag
    cancellationToken);

// Event with context
await _eventPublisher.PublishAsync(
    new OrderPlacedEvent { OrderId = orderId },
    "DataWarehouse", // Context tag
    cancellationToken);

Using Mediation Settings

More flexible approach using mediation settings for multiple tags or predicates:

// Command with multiple context tags
await _commandMediator.SendAsync(
    command,
    new CommandMediationSettings
    {
        Filters = { Tags = ["Admin", "Reporting"] }
    },
    cancellationToken);

// Query with multiple context tags
var result = await _queryMediator.QueryAsync(
    query,
    new QueryMediationSettings
    {
        Filters = { Tags = ["WebApp", "FullDetails"] }
    },
    cancellationToken);

// Event with predicate filter
await _eventPublisher.PublishAsync(
    @event,
    new EventMediationSettings
    {
        Filters = 
        { 
            Tags = ["CustomerNotification"],
            HandlerPredicate = type => type.GetCustomAttributes(typeof(EnrichedDataAttribute), false).Any()
        }
    },
    cancellationToken);

Accessing Tags in Handlers

The current execution context contains the tags specified during mediation:

public Task HandleAsync(UpdateUserCommand command, CancellationToken cancellationToken = default)
{
    var tags = AmbientExecutionContext.Current.Tags;
    
    if (tags.Contains("Admin"))
    {
        // Apply admin-specific processing
    }
    else if (tags.Contains("PublicAPI"))
    {
        // Apply public API-specific processing
    }
    
    // Continue with normal processing
    return Task.CompletedTask;
}

Best Practices for Contextual Handling

  1. Choose the right approach:

    • Use tags for static, well-defined contexts that are known at development time
    • Use predicates for dynamic filtering based on runtime conditions or complex rules
  2. Develop a consistent tagging strategy - Create a well-defined set of tags used throughout your application

  3. Document your filtering mechanisms - Maintain documentation of all tags and predicates

  4. Use marker interfaces for clean predicate filters - Marker interfaces make code more readable than complex type checking

  5. Untagged and unfiltered handlers are universal - Remember that handlers without tags execute in all contexts

  6. Use private constants for tags - Define tags as constants to avoid string typos

public static class Contexts
{
    public const string PublicApi = "PublicAPI";
    public const string AdminPortal = "AdminPortal";
    public const string InternalService = "InternalService";
    public const string ReportingSystem = "ReportingSystem";
    public const string HighPriority = "HighPriority";
}

// Usage
[HandlerTag(Contexts.PublicApi)]
public class ExternalApiValidator : ICommandValidator<CreateUserCommand> { /* ... */ }

// When sending
await _commandMediator.SendAsync(command, Contexts.PublicApi);

Contextual scenario handling with LiteBus provides a clean, maintainable way to implement different processing behaviors based on the operational context, without cluttering your code with conditional logic or duplicating message types.

Clone this wiki locally