Skip to content

Commit

Permalink
Adding support for Order as an integer property.
Browse files Browse the repository at this point in the history
Should resolve #987.

Signed off by: Bryan Boettcher <bryan.boettcher@gmail.com>
  • Loading branch information
bryanboettcher committed Dec 26, 2023
1 parent 6baaeef commit 9167b53
Show file tree
Hide file tree
Showing 11 changed files with 368 additions and 4 deletions.
35 changes: 35 additions & 0 deletions src/MediatR/Internal/HandlerExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System;
using System.Collections.Concurrent;
using System.Reflection;

namespace MediatR.Internal;

/// <summary>
/// Contains utility methods for working with implementations of request
/// handlers.
/// </summary>
public static class HandlerExtensions
{
private static readonly ConcurrentDictionary<Type, PropertyInfo?> PropertyCache = new();

/// <summary>
/// Inspects the <paramref name="value"/> parameter for a public Order property
/// that is gettable and an integer. If found, it uses the value of this property,
/// otherwise returning null.
/// </summary>
/// <param name="value">The value to inspect.</param>
/// <returns>The value of the "Order" property or null if not found.</returns>
public static int? GetOrderIfExists(this object value)
{
var orderProperty = PropertyCache.GetOrAdd(value.GetType(), valueType =>
{
var orderProperty = valueType.GetProperty("Order");
return orderProperty ?? null;
});

var orderValue = orderProperty?.GetValue(value);
if (orderValue is not int i) return null;

return i;
}
}
29 changes: 27 additions & 2 deletions src/MediatR/Internal/ObjectDetails.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using MediatR.Pipeline;

namespace MediatR.Internal;

Expand Down Expand Up @@ -40,7 +42,30 @@ public int Compare(ObjectDetails? x, ObjectDetails? y)
return -1;
}

return CompareByAssembly(x, y) ?? CompareByNamespace(x, y) ?? CompareByLocation(x, y);
return CompareByOrder(x, y) ?? CompareByAssembly(x, y) ?? CompareByNamespace(x, y) ?? CompareByLocation(x, y);
}

/// <summary>
/// Compare two objects according to an optional Order property that has been declared on many of the
/// IRequestXXXHandler interfaces.
/// </summary>
/// <param name="x">First object to compare</param>
/// <param name="y">Second object to compare</param>
/// <returns>-1 if the left object should be ordered sooner, or 1 if the right object
/// should be ordered sooner. If either object does not support the Order
/// property, or their orders match, then null is returned to allow pass-through
/// handling.</returns>
private int? CompareByOrder(ObjectDetails x, ObjectDetails y)
{
var leftOrder = x.Value.GetOrderIfExists();
var rightOrder = y.Value.GetOrderIfExists();

if (leftOrder is null && rightOrder is null) return null;

if (leftOrder is null) return 1; // always sort null after values
if (rightOrder is null) return -1; // same

return leftOrder.Value.CompareTo(rightOrder.Value);
}

/// <summary>
Expand Down Expand Up @@ -112,7 +137,7 @@ public int Compare(ObjectDetails? x, ObjectDetails? y)
/// <param name="x">First object to compare</param>
/// <param name="y">Second object to compare</param>
/// <returns>
/// An object has a higher priority if it location is part of the current location and the other is not;
/// An object has a higher priority if its location is part of the current location and the other is not;
/// If both objects are part of the current location, the closest has higher priority;
/// If none of the objects are part of the current location, they can be considered equal.
/// </returns>
Expand Down
14 changes: 14 additions & 0 deletions src/MediatR/Pipeline/IOrderableHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace MediatR.Pipeline;

/// <summary>
/// Optional interface that can be used to decorate any
/// existing handlers to add an Order property
/// </summary>
public interface IOrderableHandler
{
/// <summary>
/// Defines the order this handler should execute relative to others. Smaller numbers
/// execute sooner, and negative numbers are supported.
/// </summary>
int Order { get; }
}
11 changes: 11 additions & 0 deletions src/MediatR/Pipeline/IRequestExceptionAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ public interface IRequestExceptionAction<in TRequest, in TException>
where TRequest : notnull
where TException : Exception
{
#if NET8_0
/// <summary>
/// Controls execution order of any implementations of this request exception-action.
/// All implementations are ordered by this field, and order of duplicate
/// numbers is not guaranteed. Not overriding this property has the behavior of
/// all implementations will be executed in the order they are returned by the DI
/// container.
/// </summary>
int Order => 0;
#endif

/// <summary>
/// Called when the request handler throws an exception
/// </summary>
Expand Down
11 changes: 11 additions & 0 deletions src/MediatR/Pipeline/IRequestExceptionHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@ public interface IRequestExceptionHandler<in TRequest, TResponse, in TException>
where TRequest : notnull
where TException : Exception
{
#if NET8_0
/// <summary>
/// Controls execution order of any implementations of this request exception-processor.
/// All implementations are ordered by this field, and order of duplicate
/// numbers is not guaranteed. Not overriding this property has the behavior of
/// all implementations will be executed in the order they are returned by the DI
/// container.
/// </summary>
int Order => 0;
#endif

/// <summary>
/// Called when the request handler throws an exception
/// </summary>
Expand Down
11 changes: 11 additions & 0 deletions src/MediatR/Pipeline/IRequestPostProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@ namespace MediatR.Pipeline;
/// <typeparam name="TResponse">Response type</typeparam>
public interface IRequestPostProcessor<in TRequest, in TResponse> where TRequest : notnull
{
#if NET8_0
/// <summary>
/// Controls execution order of any implementations of this request post-processor.
/// All implementations are ordered by this field, and order of duplicate
/// numbers is not guaranteed. Not overriding this property has the behavior of
/// all implementations will be executed in the order they are returned by the DI
/// container.
/// </summary>
int Order => 0;
#endif

/// <summary>
/// Process method executes after the Handle method on your handler
/// </summary>
Expand Down
11 changes: 11 additions & 0 deletions src/MediatR/Pipeline/IRequestPreProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@ namespace MediatR.Pipeline;
/// <typeparam name="TRequest">Request type</typeparam>
public interface IRequestPreProcessor<in TRequest> where TRequest : notnull
{
#if NET8_0
/// <summary>
/// Controls execution order of any implementations of this request pre-processor.
/// All implementations are ordered by this field, and order of duplicate
/// numbers is not guaranteed. Not overriding this property has the behavior of
/// all implementations will be executed in the order they are returned by the DI
/// container.
/// </summary>
int Order => 0;
#endif

/// <summary>
/// Process method executes before calling the Handle method on your handler
/// </summary>
Expand Down
13 changes: 12 additions & 1 deletion src/MediatR/Pipeline/RequestPostProcessorBehavior.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
using System.Linq;
using MediatR.Internal;

namespace MediatR.Pipeline;

using System.Collections.Generic;
Expand All @@ -21,11 +24,19 @@ public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TRe
{
var response = await next().ConfigureAwait(false);

foreach (var processor in _postProcessors)
foreach (var processor in _postProcessors.OrderBy(GetOrder))
{
await processor.Process(request, response, cancellationToken).ConfigureAwait(false);
}

return response;
}

private static int GetOrder(IRequestPostProcessor<TRequest, TResponse> arg)
{
if (arg is IOrderableHandler oh)
return oh.Order;

return arg.GetOrderIfExists().GetValueOrDefault(0);
}
}
13 changes: 12 additions & 1 deletion src/MediatR/Pipeline/RequestPreProcessorBehavior.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
using System.Linq;
using MediatR.Internal;

namespace MediatR.Pipeline;

using System.Collections.Generic;
Expand All @@ -19,11 +22,19 @@ public RequestPreProcessorBehavior(IEnumerable<IRequestPreProcessor<TRequest>> p

public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
{
foreach (var processor in _preProcessors)
foreach (var processor in _preProcessors.OrderBy(GetOrder))
{
await processor.Process(request, cancellationToken).ConfigureAwait(false);
}

return await next().ConfigureAwait(false);
}

private static int GetOrder(IRequestPreProcessor<TRequest> arg)
{
if (arg is IOrderableHandler oh)
return oh.Order;

return arg.GetOrderIfExists().GetValueOrDefault(0);
}
}
86 changes: 86 additions & 0 deletions test/MediatR.Tests/Pipeline/Ordering/OrderedExceptionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Lamar;
using MediatR.Internal;
using MediatR.Pipeline;
using Shouldly;
using Xunit;

namespace MediatR.Tests.Pipeline.Ordering;
public class OrderedExceptionTests
{
public class MyRequest : IRequest<MyResponse>
{
public string Message { get; set; } = "initial";
}

public class MyResponse
{
public string? Message { get; set; }
}

public class DefaultHandler : IRequestHandler<MyRequest, MyResponse>
{
public Task<MyResponse> Handle(MyRequest request, CancellationToken cancellationToken) =>
throw new InvalidOperationException("this should break");
}

public class DefaultExceptionHandler : IRequestExceptionHandler<MyRequest, MyResponse, InvalidOperationException>
{
public Task Handle(MyRequest request, InvalidOperationException exception, RequestExceptionHandlerState<MyResponse> state,
CancellationToken cancellationToken)
{
state.SetHandled(new MyResponse { Message = "default-exception" });
return Task.CompletedTask;
}
}

public class FirstExceptionHandler : IRequestExceptionHandler<MyRequest, MyResponse, InvalidOperationException>
{
public int Order => -100;

public Task Handle(MyRequest request, InvalidOperationException exception, RequestExceptionHandlerState<MyResponse> state,
CancellationToken cancellationToken)
{
state.SetHandled(new MyResponse { Message = "first-exception" });
return Task.CompletedTask;
}
}


[Fact]
public async Task Should_run_exceptionprocessors()
{
var container = new Container(cfg =>
{
cfg.Scan(scanner =>
{
scanner.AssemblyContainingType(typeof(DefaultHandler));
scanner.IncludeNamespaceContainingType<MyRequest>();
scanner.WithDefaultConventions();
scanner.AddAllTypesOf(typeof(IRequestHandler<,>));
scanner.AddAllTypesOf(typeof(IRequestExceptionHandler<,,>));
});
cfg.For(typeof(IPipelineBehavior<,>)).Add(typeof(RequestExceptionProcessorBehavior<,>));
cfg.For<IMediator>().Use<Mediator>();
});

var mediator = container.GetInstance<IMediator>();

var response = await mediator.Send(new MyRequest());

// the exception handler just resets the message, so this should be the correct one
response.Message.ShouldBe("first-exception");
}

[Fact]
public void Order_is_discoverable()
{
(new DefaultExceptionHandler()).GetOrderIfExists().GetValueOrDefault(0).ShouldBe(0);
(new FirstExceptionHandler()).GetOrderIfExists().GetValueOrDefault(0).ShouldBe(-100);
}
}
Loading

0 comments on commit 9167b53

Please sign in to comment.