Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding support for Order as an integer property. #988

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading