PipelineNet icon indicating copy to clipboard operation
PipelineNet copied to clipboard

[Feature] : Interceptors first class support

Open chrisbewz opened this issue 4 months ago • 0 comments

Hi @ipvalverde,

I was recently using your library and though that is would be a good idea to support middleware interceptors. Not in the same line of thought as described on #30, but in my opinion at least support the following concept:

  • Run tasks before a given middleware (e.g. BeforeRun)
  • Run tasks after a given middleware (e.g. AfterRun)

This way we could chain interceptors either on a single middleware, or the entire pipeline.

I have worked on a sample repo with this feature implemented through decoration . But the idea is simple:

  1. Add the interceptors globally (applied on all middlewares or locally to a single one) via extension methods:
/// <summary>
    /// Registers a interceptor that will be applied to all container registered middlewares
    /// </summary>
    /// <param name="services">The service collection</param>
    /// <typeparam name="TInterceptor">Type of the interceptor to register</typeparam>
    /// <returns></returns>
    public static IServiceCollection AddInterceptor<TInterceptor>(
        this IServiceCollection services)
        where TInterceptor : class, IInterceptor
    {
        return services.AddInterceptor(typeof(TInterceptor), typeof(object), true); // typeof(object) = global
    }

    /// <summary>
    /// Registers an interceptor by type, optionally bound to a specific middleware
    /// </summary>
    /// <param name="services">The service collection</param>
    /// <param name="interceptorType">Type of the interceptor to register</param>
    /// <param name="middlewareType">Interceptor associated middleware</param>
    /// <param name="registerConfiguration">Whether to additionally register a configuration instance containing the specified interceptor and middleware types information.
    /// Defaults to false, use this parameters when attempting to registration global interceptors that will be consumed by all registered middlewares </param>
    /// <returns></returns>
    /// <exception cref="ArgumentException">Invalid interceptor/middleware type specified</exception>
    internal static IServiceCollection AddInterceptor(
        this IServiceCollection services,
        Type interceptorType,
        Type? middlewareType = null,
        bool registerConfiguration = false);

Adding to each registered middleware separately:

/// <summary>
    /// Registers a middleware with interceptor configuration
    /// </summary>
    /// <param name="services">The service collection</param>
    /// <param name="lifetime">Lifetime used to register the middleware on container</param>
    /// <param name="configure">action to configure interceptions for current registering middleware.
    /// </param>
    /// <typeparam name="TMiddleware">Type associated with the middleware to be registered</typeparam>
    public static IServiceCollection AddMiddleware<TMiddleware>(
        this IServiceCollection services,
        ServiceLifetime lifetime,
        Action<IMiddlewareInterceptorOptionsBuilder>? configure)
        where TMiddleware : class

IMiddlewareInterceptorOptionsBuilder would expose the interceptor configuration "per-middleware":

public interface IMiddlewareInterceptorOptionsBuilder
    {
       
        IMiddlewareInterceptorOptionsBuilder AddInterceptor(System.Type interceptorType);
       // (...) other interceptors related extensions
    }

Each registered interceptor results in 2 DI container registrations:

  • The interceptor instance itself
  • An IMiddlewareInterceptorConfiguration instance
internal sealed class MiddlewareInterceptorConfiguration(Type middlewareType, ImmutableList<Type> interceptorTypes)
    : IMiddlewareInterceptorConfiguration
{
    public Type MiddlewareType { get; } = middlewareType;
    public ImmutableList<Type> InterceptorTypes { get; } = interceptorTypes ?? ImmutableList<Type>.Empty;
}

This way we could implement a custom middleware resolver that:

  • Resolves all the registered interceptor configurations from scope
  • Knows how to wrap the middleware in such a decorator that forwards the call to the interceptors before and after running the middleware.

The idea here was to support:

  • Focus on configuring interception on DI instead the existing implementations
  • Let only the middleware resolver decide how to resolve and wrap middlewares with the decorators from previously registered configurations
  • Preserve the current implementation design using decorators instead implementing additional traits on existing ones

Points still to evaluate:

  • Since the middlewares and pipelines lacks a common interface to define something "runnable" I had to focus the implementation on `IAsyncMiddleware<T1,T2> to keep it simple
  • I might be wrong but the abscence of a shared "runnable" interface on middlewares and pipelines would force additional decorators to each single middleware type.

Let me know your thoughts about this idea, here is the sample repo link with some simple tests to validate the concept.

If you wish to proceed with this idea I can help with the rest of the implemenatations if necessary.

Best regards. Christian

chrisbewz avatar Oct 08 '25 14:10 chrisbewz