dotnet-cqrs/docs/core-features/dynamic-queries/interceptors.md

14 KiB

Interceptors

Advanced query customization with interceptors.

Overview

IDynamicQueryInterceptorProvider enables deep customization of PoweredSoft.DynamicQuery behavior:

  • Custom operators - Add new filter operators
  • Query transformation - Modify queries before execution
  • Logging/monitoring - Track query performance
  • Default behaviors - Override framework defaults
  • Complex filtering - Implement advanced filter logic
  • Extensibility - Extend framework capabilities

Note: Interceptors are advanced features. Most scenarios are better handled with IAlterQueryableService or IQueryableProvider.

IDynamicQueryInterceptorProvider Interface

public interface IDynamicQueryInterceptorProvider
{
    List<IInterceptor> GetInterceptors<TSource, TDestination>(
        IQueryable<TSource> queryable,
        IDynamicQuery<TSource, TDestination> query);
}

Available Interceptors

PoweredSoft.DynamicQuery provides several interceptor types:

  • IFilterInterceptor - Customize filter behavior
  • ISortInterceptor - Customize sort behavior
  • IGroupInterceptor - Customize group behavior
  • IAggregateInterceptor - Customize aggregate behavior
  • IBeforeQueryPageInterceptor - Intercept before pagination
  • IQueryConvertInterceptor - Customize result conversion

Custom Filter Operator

Implementing Custom Operator

public class CustomFilterInterceptor : IFilterInterceptor
{
    public bool CanHandle(IFilter filter)
    {
        // Handle custom "IsWeekday" operator
        return filter is Filter f && f.Type == FilterType.Custom &&
               f.CustomOperator == "IsWeekday";
    }

    public Expression<Func<T, bool>> GetExpression<T>(IFilter filter)
    {
        var path = filter.Path;

        return entity =>
        {
            var value = GetPropertyValue(entity, path) as DateTime?;
            if (!value.HasValue)
                return false;

            return value.Value.DayOfWeek != DayOfWeek.Saturday &&
                   value.Value.DayOfWeek != DayOfWeek.Sunday;
        };
    }

    private object? GetPropertyValue(object obj, string propertyPath)
    {
        var properties = propertyPath.Split('.');
        object current = obj;

        foreach (var prop in properties)
        {
            var propertyInfo = current.GetType().GetProperty(prop);
            if (propertyInfo == null)
                return null;

            current = propertyInfo.GetValue(current);
            if (current == null)
                return null;
        }

        return current;
    }
}

Provider Implementation

public class CustomInterceptorProvider : IDynamicQueryInterceptorProvider
{
    public List<IInterceptor> GetInterceptors<TSource, TDestination>(
        IQueryable<TSource> queryable,
        IDynamicQuery<TSource, TDestination> query)
    {
        return new List<IInterceptor>
        {
            new CustomFilterInterceptor()
        };
    }
}

Registration

builder.Services.AddDynamicQuery<Order, OrderDto>()
    .AddDynamicQueryWithProvider<Order, OrderQueryableProvider>()
    .AddDynamicQueryInterceptorProvider<Order, OrderDto, CustomInterceptorProvider>();

Usage

{
  "filters": [
    {
      "path": "orderDate",
      "operator": "Custom",
      "customOperator": "IsWeekday"
    }
  ]
}

Logging Interceptor

Query Logging

public class LoggingInterceptor : IBeforeQueryPageInterceptor
{
    private readonly ILogger<LoggingInterceptor> _logger;

    public LoggingInterceptor(ILogger<LoggingInterceptor> logger)
    {
        _logger = logger;
    }

    public void InterceptBeforeQueryPage<T>(IQueryable<T> queryable)
    {
        var sql = queryable.ToQueryString();

        _logger.LogInformation("Executing dynamic query: {SQL}", sql);

        var stopwatch = Stopwatch.StartNew();
        // Query will execute after this interceptor
        stopwatch.Stop();

        _logger.LogInformation("Query completed in {ElapsedMs}ms", stopwatch.ElapsedMilliseconds);
    }
}

public class LoggingInterceptorProvider : IDynamicQueryInterceptorProvider
{
    private readonly ILogger<LoggingInterceptor> _logger;

    public LoggingInterceptorProvider(ILogger<LoggingInterceptor> logger)
    {
        _logger = logger;
    }

    public List<IInterceptor> GetInterceptors<TSource, TDestination>(
        IQueryable<TSource> queryable,
        IDynamicQuery<TSource, TDestination> query)
    {
        return new List<IInterceptor>
        {
            new LoggingInterceptor(_logger)
        };
    }
}

Performance Monitoring

Query Performance Interceptor

public class PerformanceInterceptor : IBeforeQueryPageInterceptor
{
    private readonly IMetricsCollector _metrics;

    public PerformanceInterceptor(IMetricsCollector metrics)
    {
        _metrics = metrics;
    }

    public void InterceptBeforeQueryPage<T>(IQueryable<T> queryable)
    {
        var stopwatch = Stopwatch.StartNew();

        // Record query execution time
        var expression = queryable.Expression.ToString();
        var complexity = CalculateComplexity(expression);

        _metrics.RecordQueryExecution(new QueryMetrics
        {
            Complexity = complexity,
            Timestamp = DateTime.UtcNow,
            QueryExpression = expression
        });
    }

    private int CalculateComplexity(string expression)
    {
        // Simple complexity calculation based on expression length and operations
        var whereCount = Regex.Matches(expression, "Where").Count;
        var joinCount = Regex.Matches(expression, "Join").Count;
        var orderByCount = Regex.Matches(expression, "OrderBy").Count;

        return whereCount + (joinCount * 2) + orderByCount;
    }
}

Default Value Interceptor

Auto-Apply Default Filters

public class DefaultFilterInterceptor : IFilterInterceptor
{
    public bool CanHandle(IFilter filter)
    {
        // This interceptor handles all filters to add defaults
        return true;
    }

    public Expression<Func<T, bool>> GetExpression<T>(IFilter filter)
    {
        // If filtering on a date and no time component specified,
        // default to start of day
        if (filter.Path.Contains("Date") && filter.Value is DateTime date)
        {
            if (date.TimeOfDay == TimeSpan.Zero)
            {
                // Adjust filter to match entire day
                var endOfDay = date.AddDays(1);

                return entity =>
                {
                    var value = GetPropertyValue(entity, filter.Path) as DateTime?;
                    return value >= date && value < endOfDay;
                };
            }
        }

        // Let default handling proceed
        return null;
    }
}

Case-Insensitive Filter

Case-Insensitive String Matching

public class CaseInsensitiveFilterInterceptor : IFilterInterceptor
{
    public bool CanHandle(IFilter filter)
    {
        return filter is Filter f &&
               f.Type == FilterType.String &&
               f.Value is string;
    }

    public Expression<Func<T, bool>> GetExpression<T>(IFilter filter)
    {
        var f = filter as Filter;
        var searchValue = (f.Value as string)?.ToLower();

        switch (f.Operator)
        {
            case FilterOperator.Contains:
                return entity =>
                {
                    var value = GetPropertyValue(entity, f.Path) as string;
                    return value != null && value.ToLower().Contains(searchValue);
                };

            case FilterOperator.StartsWith:
                return entity =>
                {
                    var value = GetPropertyValue(entity, f.Path) as string;
                    return value != null && value.ToLower().StartsWith(searchValue);
                };

            case FilterOperator.EndsWith:
                return entity =>
                {
                    var value = GetPropertyValue(entity, f.Path) as string;
                    return value != null && value.ToLower().EndsWith(searchValue);
                };

            default:
                return null; // Use default handling
        }
    }
}

Query Validation Interceptor

Validate Query Complexity

public class QueryValidationInterceptor : IBeforeQueryPageInterceptor
{
    private const int MaxFilterCount = 10;
    private const int MaxSortCount = 5;

    public void InterceptBeforeQueryPage<T>(IQueryable<T> queryable)
    {
        var expression = queryable.Expression.ToString();

        var filterCount = Regex.Matches(expression, "Where").Count;
        var sortCount = Regex.Matches(expression, "OrderBy").Count;

        if (filterCount > MaxFilterCount)
        {
            throw new InvalidOperationException(
                $"Query has too many filters ({filterCount}). Maximum is {MaxFilterCount}.");
        }

        if (sortCount > MaxSortCount)
        {
            throw new InvalidOperationException(
                $"Query has too many sorts ({sortCount}). Maximum is {MaxSortCount}.");
        }
    }
}

Composite Interceptor Provider

Multiple Interceptors

public class CompositeInterceptorProvider : IDynamicQueryInterceptorProvider
{
    private readonly ILogger<LoggingInterceptor> _logger;
    private readonly IMetricsCollector _metrics;

    public CompositeInterceptorProvider(
        ILogger<LoggingInterceptor> logger,
        IMetricsCollector metrics)
    {
        _logger = logger;
        _metrics = metrics;
    }

    public List<IInterceptor> GetInterceptors<TSource, TDestination>(
        IQueryable<TSource> queryable,
        IDynamicQuery<TSource, TDestination> query)
    {
        return new List<IInterceptor>
        {
            new LoggingInterceptor(_logger),
            new PerformanceInterceptor(_metrics),
            new QueryValidationInterceptor(),
            new CaseInsensitiveFilterInterceptor(),
            new CustomFilterInterceptor()
        };
    }
}

Testing Interceptors

Unit Tests

public class CustomFilterInterceptorTests
{
    private readonly CustomFilterInterceptor _interceptor;

    public CustomFilterInterceptorTests()
    {
        _interceptor = new CustomFilterInterceptor();
    }

    [Fact]
    public void CanHandle_WithIsWeekdayOperator_ReturnsTrue()
    {
        var filter = new Filter
        {
            Path = "orderDate",
            Type = FilterType.Custom,
            CustomOperator = "IsWeekday"
        };

        var result = _interceptor.CanHandle(filter);

        Assert.True(result);
    }

    [Theory]
    [InlineData("2024-01-01")] // Monday
    [InlineData("2024-01-02")] // Tuesday
    [InlineData("2024-01-05")] // Friday
    public void GetExpression_WithWeekday_ReturnsTrue(string dateString)
    {
        var filter = new Filter
        {
            Path = "orderDate",
            Type = FilterType.Custom,
            CustomOperator = "IsWeekday"
        };

        var expression = _interceptor.GetExpression<Order>(filter);
        var compiled = expression.Compile();

        var order = new Order { OrderDate = DateTime.Parse(dateString) };

        Assert.True(compiled(order));
    }

    [Theory]
    [InlineData("2024-01-06")] // Saturday
    [InlineData("2024-01-07")] // Sunday
    public void GetExpression_WithWeekend_ReturnsFalse(string dateString)
    {
        var filter = new Filter
        {
            Path = "orderDate",
            Type = FilterType.Custom,
            CustomOperator = "IsWeekday"
        };

        var expression = _interceptor.GetExpression<Order>(filter);
        var compiled = expression.Compile();

        var order = new Order { OrderDate = DateTime.Parse(dateString) };

        Assert.False(compiled(order));
    }
}

When to Use Interceptors

Use Interceptors For:

  • Custom filter operators not supported by framework
  • Query logging and monitoring
  • Performance tracking
  • Query validation
  • Default value injection
  • Complex filter logic

Use Alternatives For:

  • Security filtering → Use IAlterQueryableService
  • Tenant isolation → Use IAlterQueryableService
  • Default base filters → Use IQueryableProvider
  • Simple customization → Use query parameters

Best Practices

DO

  • Keep interceptors focused and single-purpose
  • Test interceptors independently
  • Document custom operators
  • Use descriptive operator names
  • Return null to use default handling
  • Log complex query executions

DON'T

  • Don't use interceptors for security (use IAlterQueryableService)
  • Don't perform synchronous I/O in interceptors
  • Don't modify queryable in filter interceptors
  • Don't throw exceptions without validation
  • Don't create overly complex interceptors
  • Don't skip testing custom operators

Common Patterns

Pattern: Conditional Interceptor

public class ConditionalInterceptorProvider : IDynamicQueryInterceptorProvider
{
    private readonly IWebHostEnvironment _environment;
    private readonly ILogger<LoggingInterceptor> _logger;

    public ConditionalInterceptorProvider(
        IWebHostEnvironment environment,
        ILogger<LoggingInterceptor> logger)
    {
        _environment = environment;
        _logger = logger;
    }

    public List<IInterceptor> GetInterceptors<TSource, TDestination>(
        IQueryable<TSource> queryable,
        IDynamicQuery<TSource, TDestination> query)
    {
        var interceptors = new List<IInterceptor>();

        // Only log in development
        if (_environment.IsDevelopment())
        {
            interceptors.Add(new LoggingInterceptor(_logger));
        }

        // Always add custom filters
        interceptors.Add(new CustomFilterInterceptor());

        return interceptors;
    }
}

See Also