Adam Anderly

4 min read

Cross-Cutting Concerns with MediatR Pipeline Behaviors

Filed under .NET, ASP.NET Core, MediatR

I’ve recently been exploring MediatR Pipeline Behaviors and cross-cutting concerns (logging, validation, security, transactions) but also wanted to tackle some performance and resilience scenarios that may not always apply to every request/command (retry, fallback and caching).

Here are some gists that tackle these concerns:

Caching Behavior (using ASP.NET Core IDistributedCache):

For caching, I’m using a separately defined CachePolicy similar to a FluentValidation Validator class.

CacheService.cs
public class CacheService : ICache
	{
		private readonly string _keyPrefix;
		private readonly IDistributedCache _cache;
		private readonly IConfiguration _config;

		public CacheService(IDistributedCache cache, IConfiguration config)
		{
			_cache = cache;
			_config = config;
			var keyPrefix = _config["CacheKeyPrefix"];
			_keyPrefix = keyPrefix ?? typeof(CacheService).FullName;
		}

		public string GetCacheKey(string key)
		{
			return MakeKey(key);
		}

		public async Task<T> GetAsync<T>(string key, CancellationToken token = default)
		{
			return await GetInternalAsync<T>(key, token);
		}

		public async Task<T> GetOrCreateAsync<T>(string key, Func<Task<T>> factory, CancellationToken token = default)
		{
			return await GetOrCreateInternalAsync<T>(key, null, null, null, factory, token);
		}

		public async Task<T> GetOrCreateAsync<T>(string key, TimeSpan slidingExpiration, Func<Task<T>> factory, CancellationToken token = default)
		{
			return await GetOrCreateInternalAsync<T>(key, slidingExpiration, null, null, factory, token);
		}

		public async Task<T> GetOrCreateAsync<T>(string key, DateTime absoluteExpiration, Func<Task<T>> factory, CancellationToken token = default)
		{
			return await GetOrCreateInternalAsync<T>(key, null, absoluteExpiration, null, factory, token);
		}

		public async Task<T> GetOrCreateAsync<T>(string key, TimeSpan slidingExpiration, DateTime absoluteExpiration, Func<Task<T>> factory, CancellationToken token = default)
		{
			return await GetOrCreateInternalAsync<T>(key, slidingExpiration, absoluteExpiration, null, factory, token);
		}

		public async Task<T> GetOrCreateAsync<T>(string key, TimeSpan slidingExpiration, TimeSpan absoluteExpirationRelativeToNow, Func<Task<T>> factory, CancellationToken token = default)
		{
			return await GetOrCreateInternalAsync<T>(key, slidingExpiration, null, absoluteExpirationRelativeToNow, factory, token);
		}

		public async Task<T> GetOrCreateAsync<T>(string key, TimeSpan? slidingExpiration, DateTime? absoluteExpiration, TimeSpan? absoluteExpirationRelativeToNow, Func<Task<T>> factory, CancellationToken token = default)
		{
			return await GetOrCreateInternalAsync<T>(key, slidingExpiration, absoluteExpiration, absoluteExpirationRelativeToNow, factory, token);
		}

		public async Task SetAsync<T>(string key, T value, CancellationToken token = default)
		{
			await SetInternalAsync(key, value, null, null, null, token);
		}

		public async Task SetAsync<T>(string key, T value, DateTime absoluteExpiration,
			CancellationToken token = default)
		{
			await SetInternalAsync(key, value, null, absoluteExpiration, null, token);
		}

		public async Task SetAsync<T>(string key, T value, TimeSpan slidingExpiration,
			CancellationToken token = default)
		{
			await SetInternalAsync(key, value, slidingExpiration, null, null, token);
		}

		public async Task SetAsync<T>(string key, T value, TimeSpan? slidingExpiration, DateTime? absoluteExpiration,
			CancellationToken token = default)
		{
			await SetInternalAsync(key, value, slidingExpiration, absoluteExpiration, null, token);
		}

		public async Task SetAsync<T>(string key, T value, TimeSpan? slidingExpiration, TimeSpan? absoluteExpirationRelativeToNow,
			CancellationToken token = default)
		{
			await SetInternalAsync(key, value, slidingExpiration, null, absoluteExpirationRelativeToNow, token);
		}

		public async Task SetAsync<T>(string key, T value, TimeSpan? slidingExpiration, DateTime? absoluteExpiration,
			TimeSpan? absoluteExpirationRelativeToNow, CancellationToken token = default)
		{
			await SetInternalAsync(key, value, slidingExpiration, absoluteExpiration, absoluteExpirationRelativeToNow, token);
		}

		public async Task RefreshAsync(string key, CancellationToken token = default)
		{
			await _cache.RefreshAsync(key, token);
		}

		public async Task RemoveAsync(string key, CancellationToken token = default)
		{
			await _cache.RemoveAsync(key, token);
		}

		private string MakeKey(string key)
		{
			return $"{(string.IsNullOrWhiteSpace(_keyPrefix) ? "" : _keyPrefix + ":")}{key}";
		}

		private async Task<T> GetInternalAsync<T>(string key, CancellationToken token = default)
		{
			return await _cache.GetAsync<T>(MakeKey(key), token);
		}

		private async Task<T> GetOrCreateInternalAsync<T>(string key, TimeSpan? slidingExpiration, DateTime? absoluteExpiration, TimeSpan? absoluteExpirationRelativeToNow, Func<Task<T>> factory, CancellationToken token = default)
		{
			var value = await GetInternalAsync<T>(key, token);

			if (value != null) return value;

			value = await factory();

			if (value != null)
			{
				await SetInternalAsync<T>(key, value, slidingExpiration, absoluteExpiration, absoluteExpirationRelativeToNow, token);
			}

			return value;
		}

		private async Task SetInternalAsync<T>(string key, T value, TimeSpan? slidingExpiration, DateTime? absoluteExpiration, TimeSpan? absoluteExpirationRelativeToNow,
			CancellationToken token = default)
		{
			var cacheEntryOptions = new DistributedCacheEntryOptions();
			if (slidingExpiration.HasValue)
			{
				cacheEntryOptions.SlidingExpiration = slidingExpiration.Value;
			}

			if (absoluteExpiration.HasValue)
			{
				cacheEntryOptions.AbsoluteExpiration = absoluteExpiration.Value;
			}

			if (absoluteExpirationRelativeToNow.HasValue)
			{
				cacheEntryOptions.AbsoluteExpirationRelativeToNow = absoluteExpirationRelativeToNow.Value;
			}

			if (!slidingExpiration.HasValue && !absoluteExpiration.HasValue && !absoluteExpirationRelativeToNow.HasValue)
			{
				cacheEntryOptions.SetSlidingExpiration(TimeSpan.FromSeconds(30));
				cacheEntryOptions.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
				//cacheEntryOptions.SetAbsoluteExpiration(DateTime.UtcNow.AddMinutes(5));
			}

			await _cache.SetAsync<T>(MakeKey(key), value, cacheEntryOptions, token);
		}

	}
CachingBehavior.cs
/// <summary>
/// MediatR Caching Pipeline Behavior
/// </summary>
/// <typeparam name="TRequest"></typeparam>
/// <typeparam name="TResponse"></typeparam>
public class CachingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse>
{
    private readonly IEnumerable<ICachePolicy<TRequest, TResponse>> _cachePolicies;
    // ICache is a helper wrapper over IDistributedCache that adds some read-through cache methods, etc.
    private readonly ICache _cache;
    private readonly ILogger<CachingBehavior<TRequest, TResponse>> _logger;

    public CachingBehavior(ICache cache, ILogger<CachingBehavior<TRequest, TResponse>> logger, IEnumerable<ICachePolicy<TRequest, TResponse>> cachePolicies)
    {
        _cache = cache;
        _logger = logger;
        _cachePolicies = cachePolicies;
    }

    public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
    {
        var cachePolicy = _cachePolicies.FirstOrDefault();
        if (cachePolicy == null)
        {
            // No cache policy found, so just continue through the pipeline
            return await next();
        }
        var cacheKey = cachePolicy.GetCacheKey(request);
        var cachedResponse = await _cache.GetAsync<TResponse>(cacheKey, cancellationToken);
        if (cachedResponse != null)
        {
            _logger.LogDebug($"Response retrieved {typeof(TRequest).FullName} from cache. CacheKey: {cacheKey}");
            return cachedResponse;
        }

        var response = await next();
        _logger.LogDebug($"Caching response for {typeof(TRequest).FullName} with cache key: {cacheKey}");

        await _cache.SetAsync(cacheKey, response, cachePolicy.SlidingExpiration, cachePolicy.AbsoluteExpiration, cachePolicy.AbsoluteExpirationRelativeToNow, cancellationToken);
        return response;
    }
 }
Details.cs
public class Details
{
  
    // Query definition
    public class Query : IRequest<Models.Customer>
    {
        [FromRoute]
        public string CustomerNumber { get; set; }
    }
    
    // Simply defining a CachePolicy for ICachePolicy<TRequest,TResponse> sets up caching
    // similar to setting up a FluentValidation Validator that inherits from AbstractValidator<TRequest>.
    // This could be in the same file or in a separate file, but doesn't clutter up the "Handler".
    public class CachePolicy : ICachePolicy<Query, Models.Customer>
    {
        // Optionally, change defaults
        public TimeSpan? AbsoluteExpirationRelativeToNow => TimeSpan.FromMinutes(10);
        public TimeSpan? SlidingExpiration => TimeSpan.FromMinutes(1);

        // Optionally, provide a different implementation here. By default the CacheKey will be in the following format:
        //     Query{CustomerNumber:001425}
        public string GetCacheKey(Query query)
        {
            return $"Customers.{query.CustomerNumber}";
        }
    }
    
    // Query Handler
    public class Handler : IRequestHandler<Query, Models.Customer>
    {
        private readonly CustomerContext _db;
        private readonly IConfigurationProvider _configuration;

        public Handler(CustomerContext db, IConfigurationProvider configuration)
        {
            _db = db;
            _configuration = configuration;
        }

        public async Task<Models.Customer> Handle(Query message, CancellationToken token)
        {
            return await _db.Customers.Where(c => c.Number == message.CustomerNumber)
                .ProjectTo<Models.Customer>(_configuration)
                .FirstOrDefaultAsync(token);
        }
    }
    
   }
}
ICache.cs
public interface ICache
	{
		string GetCacheKey(string key);

		/// <summary>Gets a value with the given key.</summary>
		/// <param name="key">A string identifying the requested value.</param>
		/// <param name="token">Optional. The <see cref="T:System.Threading.CancellationToken" /> used to propagate notifications that the operation should be canceled.</param>
		/// <returns>The <see cref="T:System.Threading.Tasks.Task" /> that represents the asynchronous operation, containing the located value or null.</returns>
		Task<T> GetAsync<T>(string key, CancellationToken token = default);

		Task<T> GetOrCreateAsync<T>(
			string key,
			Func<Task<T>> factory,
			CancellationToken token = default);

		Task<T> GetOrCreateAsync<T>(
			string key,
			TimeSpan slidingExpiration,
			Func<Task<T>> factory,
			CancellationToken token = default);

		Task<T> GetOrCreateAsync<T>(
			string key,
			DateTime absoluteExpiration,
			Func<Task<T>> factory,
			CancellationToken token = default);

		Task<T> GetOrCreateAsync<T>(
			string key,
			TimeSpan slidingExpiration,
			DateTime absoluteExpiration,
			Func<Task<T>> factory,
			CancellationToken token = default);

		Task<T> GetOrCreateAsync<T>(
			string key,
			TimeSpan? slidingExpiration,
			DateTime? absoluteExpiration,
			TimeSpan? absoluteExpirationRelativeToNow,
			Func<Task<T>> factory,
			CancellationToken token = default);

		/// <summary>Sets the value with the given key.</summary>
		/// <param name="key">A string identifying the requested value.</param>
		/// <param name="value">The value to set in the cache.</param>
		/// <param name="token">Optional. The <see cref="T:System.Threading.CancellationToken" /> used to propagate notifications that the operation should be canceled.</param>
		/// <returns>The <see cref="T:System.Threading.Tasks.Task" /> that represents the asynchronous operation.</returns>
		Task SetAsync<T>(
			string key,
			T value,
			CancellationToken token = default);

		/// <summary>Sets the value with the given key.</summary>
		/// <param name="key">A string identifying the requested value.</param>
		/// <param name="value">The value to set in the cache.</param>
		/// <param name="absoluteExpiration">The cache options for the value.</param>
		/// <param name="token">Optional. The <see cref="T:System.Threading.CancellationToken" /> used to propagate notifications that the operation should be canceled.</param>
		/// <returns>The <see cref="T:System.Threading.Tasks.Task" /> that represents the asynchronous operation.</returns>
		Task SetAsync<T>(
			string key,
			T value,
			DateTime absoluteExpiration,
			CancellationToken token = default);

		/// <summary>Sets the value with the given key.</summary>
		/// <param name="key">A string identifying the requested value.</param>
		/// <param name="value">The value to set in the cache.</param>
		/// <param name="slidingExpiration">The cache options for the value.</param>
		/// <param name="token">Optional. The <see cref="T:System.Threading.CancellationToken" /> used to propagate notifications that the operation should be canceled.</param>
		/// <returns>The <see cref="T:System.Threading.Tasks.Task" /> that represents the asynchronous operation.</returns>
		Task SetAsync<T>(
			string key,
			T value,
			TimeSpan slidingExpiration,
			CancellationToken token = default);

		/// <summary>Sets the value with the given key.</summary>
		/// <param name="key">A string identifying the requested value.</param>
		/// <param name="value">The value to set in the cache.</param>
		/// <param name="slidingExpiration">The cache options for the value.</param>
		/// <param name="absoluteExpiration">The cache options for the value.</param>
		/// <param name="token">Optional. The <see cref="T:System.Threading.CancellationToken" /> used to propagate notifications that the operation should be canceled.</param>
		/// <returns>The <see cref="T:System.Threading.Tasks.Task" /> that represents the asynchronous operation.</returns>
		Task SetAsync<T>(
			string key,
			T value,
			TimeSpan? slidingExpiration,
			DateTime? absoluteExpiration,
			CancellationToken token = default);

		/// <summary>Sets the value with the given key.</summary>
		/// <param name="key">A string identifying the requested value.</param>
		/// <param name="value">The value to set in the cache.</param>
		/// <param name="slidingExpiration">The cache options for the value.</param>
		/// <param name="absoluteExpirationRelativeToNow">The cache options for the value.</param>
		/// <param name="token">Optional. The <see cref="T:System.Threading.CancellationToken" /> used to propagate notifications that the operation should be canceled.</param>
		/// <returns>The <see cref="T:System.Threading.Tasks.Task" /> that represents the asynchronous operation.</returns>
		Task SetAsync<T>(
			string key,
			T value,
			TimeSpan? slidingExpiration,
			TimeSpan? absoluteExpirationRelativeToNow,
			CancellationToken token = default);

		/// <summary>Sets the value with the given key.</summary>
		/// <param name="key">A string identifying the requested value.</param>
		/// <param name="value">The value to set in the cache.</param>
		/// <param name="slidingExpiration">The cache options for the value.</param>
		/// <param name="absoluteExpiration">The cache options for the value.</param>
		/// <param name="absoluteExpirationRelativeToNow">The cache options for the value.</param>
		/// <param name="token">Optional. The <see cref="T:System.Threading.CancellationToken" /> used to propagate notifications that the operation should be canceled.</param>
		/// <returns>The <see cref="T:System.Threading.Tasks.Task" /> that represents the asynchronous operation.</returns>
		Task SetAsync<T>(
			string key,
			T value,
			TimeSpan? slidingExpiration,
			DateTime? absoluteExpiration,
			TimeSpan? absoluteExpirationRelativeToNow,
			CancellationToken token = default);

		/// <summary>
		/// Refreshes a value in the cache based on its key, resetting its sliding expiration timeout (if any).
		/// </summary>
		/// <param name="key">A string identifying the requested value.</param>
		/// <param name="token">Optional. The <see cref="T:System.Threading.CancellationToken" /> used to propagate notifications that the operation should be canceled.</param>
		/// <returns>The <see cref="T:System.Threading.Tasks.Task" /> that represents the asynchronous operation.</returns>
		Task RefreshAsync(string key, CancellationToken token = default);

		/// <summary>Removes the value with the given key.</summary>
		/// <param name="key">A string identifying the requested value.</param>
		/// <param name="token">Optional. The <see cref="T:System.Threading.CancellationToken" /> used to propagate notifications that the operation should be canceled.</param>
		/// <returns>The <see cref="T:System.Threading.Tasks.Task" /> that represents the asynchronous operation.</returns>
		Task RemoveAsync(string key, CancellationToken token = default);
	}
ICachePolicy.cs
// Using C# 8.0 to provide a default interface implementation.
// Optionally, could move this to an AbstractCachingPolicy like AbstractValidator.
public interface ICachePolicy<TRequest, TResponse> where TRequest : IRequest<TResponse>
{
    DateTime? AbsoluteExpiration => null;
    TimeSpan? AbsoluteExpirationRelativeToNow => TimeSpan.FromMinutes(5);
    TimeSpan? SlidingExpiration => TimeSpan.FromSeconds(30);

    string GetCacheKey(TRequest request)
    {
        var r = new {request};
        var props = r.request.GetType().GetProperties().Select(pi => $"{pi.Name}:{pi.GetValue(r.request, null)}");
        return $"{typeof(TRequest).FullName}{{{String.Join(",", props)}}}";
    }
}

View MediatR Caching Pipeline Behavior on GitHub.

Fallback Behavior (using Polly):

For Fallback and Retry behavior, I’m using separate interfaces IFallbackHandler and IRetryableRequest to get the behavior.

Details.cs
public class Handler : IRequestHandler<Query, Customer>, IFallbackHandler<Query, Customer>
{
    private readonly ICustomerRepository _db;
    private readonly ICustomerRepository<SourceSystem.Secondary> _db2;
    private readonly IConfigurationProvider _configuration;

    public Handler(ICustomerRepository db, ICustomerRepository<SourceSystem.Secondary> db2, IConfigurationProvider configuration)
    {
        _db = db;
        _db2 = db2;
        _configuration = configuration;
    }

    public async Task<Customer> Handle(Query message, CancellationToken token)
    {
        return await _db.GetCustomerByNumberAsync(message.CustomerNumber);
    }

    // HandleFallback method from IFallbackHandler<TRequest, TResponse> interface.
    // This method will get called if my Handle method fails.
    public async Task<Customer> HandleFallback(Query message, CancellationToken token)
    {
        return await _db2.GetCustomerByNumberAsync(message.CustomerNumber);
    }
}
FallbackBehavior.cs
/// <summary>
/// MediatR Fallback Pipeline Behavior
/// </summary>
/// <typeparam name="TRequest"></typeparam>
/// <typeparam name="TResponse"></typeparam>
public class FallbackBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly IEnumerable<IFallbackHandler<TRequest, TResponse>> _fallbackHandlers;
    private readonly ILogger<FallbackBehavior<TRequest, TResponse>> _logger;

    public FallbackBehavior(IEnumerable<IFallbackHandler<TRequest, TResponse>> fallbackHandlers, ILogger<FallbackBehavior<TRequest, TResponse>> logger)
    {
        _fallbackHandlers = fallbackHandlers;
        _logger = logger;
    }

    public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
    {
        var fallbackHandler = _fallbackHandlers.FirstOrDefault();
        if (fallbackHandler == null)
        {
            // No fallback handler found, continue through pipeline
            return await next();
        }

        var fallbackPolicy = Policy<TResponse>
            .Handle<Exception>()
            .FallbackAsync(async (cancellationToken) =>
            {
                _logger.LogDebug($"Initial handler failed. Falling back to `{fallbackHandler.GetType().FullName}@HandleFallback`");
                return await fallbackHandler.HandleFallback(request, cancellationToken)
                    .ConfigureAwait(false);
            });

        var response = await fallbackPolicy.ExecuteAsync(async () => await next());

        return response;
    }
}
IFallbackHandler.cs
public interface IFallbackHandler<TRequest, TResponse> where TRequest : IRequest<TResponse>
{
    Task<TResponse> HandleFallback(TRequest request, CancellationToken cancellationToken);
}

View MediatR Fallback Pipeline Behavior on GitHub.

Retry Behavior (using Polly):

Details.cs
public class Handler : IRequestHandler<Query, Customer>, IRetryableRequest<Query, Customer>
{
    private readonly CustomerContext _db;
    private readonly IConfigurationProvider _configuration;

    // By implementing IRetryableRequest<TRequest, TResponse> I get the following
    // properties to control how my "Handle" method will get retried.
    public int RetryAttempts => 2;
    public int RetryDelay => 500;
    public bool RetryWithExponentialBackoff => true;
    public int ExceptionsAllowedBeforeCircuitTrip => 2;

    public Handler(CustomerContext db, IConfigurationProvider configuration)
    {
        _db = db;
        _configuration = configuration;
    }

    public async Task<Customer> Handle(Query message, CancellationToken token)
    {
        return await _db.Customers.Where(c => c.Number == message.CustomerNumber)
            .ProjectTo<Models.Customer>(_configuration)
            .FirstOrDefaultAsync(token);
    }

}
IRetryableRequest.cs
public interface IRetryableRequest<TRequest, TResponse> where TRequest : IRequest<TResponse>
{
    int RetryAttempts => 1;

    int RetryDelay => 250;

    bool RetryWithExponentialBackoff => false;

    int ExceptionsAllowedBeforeCircuitTrip => 1;

}
RetryBehavior.cs
public class RetryBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse>
{
    private readonly IEnumerable<IRetryableRequest<TRequest, TResponse>> _retryHandlers;
    private readonly ILogger<RetryBehavior<TRequest, TResponse>> _logger;

    public RetryBehavior(IEnumerable<IRetryableRequest<TRequest, TResponse>> retryHandlers, ILogger<RetryBehavior<TRequest, TResponse>> logger)
    {
        _retryHandlers = retryHandlers;
        _logger = logger;
    }

    public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
    {
        var retryHandler = _retryHandlers.FirstOrDefault();
        if (retryHandler == null)
        {
            // No retry handler found, continue through pipeline
            return await next();
        }

        var circuitBreaker = Policy<TResponse>
            .Handle<Exception>()
            .CircuitBreakerAsync(retryHandler.ExceptionsAllowedBeforeCircuitTrip, TimeSpan.FromMilliseconds(5000),
                (exception, things) =>
                {
                    _logger.LogDebug("Circuit Tripped!");
                },
                () =>
                {
                });

        var retryPolicy = Policy<TResponse>
            .Handle<Exception>()
            .WaitAndRetryAsync(retryHandler.RetryAttempts, retryAttempt =>
            {
                var retryDelay = retryHandler.RetryWithExponentialBackoff
                    ? TimeSpan.FromMilliseconds(Math.Pow(2, retryAttempt) * retryHandler.RetryDelay)
                    : TimeSpan.FromMilliseconds(_handler.RetryDelay);

                _logger.LogDebug($"Retrying, waiting {retryDelay}...");

                return retryDelay;
            });

        var response = await retryPolicy.ExecuteAsync(async () => await next());

        return response;
    }
}

View MediatR Retry Pipeline Behavior on GitHub.

These can all be combined as well and work based on the order they’re registered in Startup.cs. In my case, I want my main Handle method to retry, then, optionally, fallback to my HandleFallback, then finally cache the result based on the defined CachePolicy.

I’m debating between using separately defined classes/policies (like FluentValidation Validators) vs. making Handlers implement the interfaces to get the behavior. On one hand, if they’re separate, I could move the Cache, Fallback and Retry policies to separate files, folders, etc. and keep my main MediatR handlers very lean and clean. If you want to see them in the same place, you could put them in the same file like many of Jimmy Bogard’s examples with Validators, etc. But if the files get too big and you want to separate them out, you could do that. However, with Retry and Fallback, it’s kind of nice seeing that with the main request handler.

Of course, all this gets wired-up and registered in Startup.cs. I’m using Scrutor to aid in discovery my Caching, Fallback and Retry policies. See below.

Startup.cs
// Register MediatR Pipeline Behaviors
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(CachingBehavior<,>));
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(FallbackBehavior<,>));
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(RetryBehavior<,>));

// ICachePolicy discovery and registration
services.Scan(scan => scan
    .FromAssemblies(assembly)
    .AddClasses(classes => classes.AssignableTo(typeof(ICachePolicy<,>)))
    .AsImplementedInterfaces()
    .WithTransientLifetime());

// IFallbackHandler discovery and registration
services.Scan(scan => scan
    .FromAssemblies(typeof(Startup).Assembly)
    .AddClasses(classes => classes.AssignableTo(typeof(IFallbackHandler<,>)))
    .UsingRegistrationStrategy(RegistrationStrategy.Skip)
    .AsImplementedInterfaces()
    .WithTransientLifetime());

// IFallbackHandler discovery and registration
services.Scan(scan => scan
    .FromAssemblies(typeof(Startup).Assembly)
    .AddClasses(classes => classes.AssignableTo(typeof(IRetryableRequest<,>)))
    .UsingRegistrationStrategy(RegistrationStrategy.Skip)
    .AsImplementedInterfaces()
    .WithTransientLifetime());

View MediatR Pipeline Behavior Registration on GitHub.

What do you think? Which approach do you prefer? Let me know in the comments.

Comments