Skip to content

Commit

Permalink
merge
Browse files Browse the repository at this point in the history
  • Loading branch information
JimGeersinga committed Jul 10, 2024
2 parents 2f1ab32 + 81244e2 commit 5ecd2cf
Show file tree
Hide file tree
Showing 9 changed files with 248 additions and 112 deletions.
46 changes: 30 additions & 16 deletions MediatR.Chained.Console/App.cs
Original file line number Diff line number Diff line change
@@ -1,52 +1,66 @@
using MediatR;
using ErrorOr;

using MediatR;
using MediatR.Chained;

internal class App(IMediator mediator)
{
public async Task RunAsync()
{
Console.WriteLine("Normal:");

await mediator.Send(new TestCommand1("Hello"));
await mediator.Send(new TestCommand2("Hello", "World"));
await mediator.Send(new TestCommand3("Hello", "World", "Again"));

Console.WriteLine("Chained:");

await mediator
IErrorOr? result = await mediator
.Add(new TestCommand1("Hello"))
.FailWhen(x => x.IsError)
.Add(x => new TestCommand2(x.Value, "World"))
.Add(x => new TestCommand3(x.Value.Item1, x.Value.Item2, "Again"))
.SendAsync<IErrorOr>();

Console.WriteLine(result!.IsError);

object? result2 = await mediator
.Add(new TestCommand1("Hello"))
.Add(x => new TestCommand2(x, "World"))
.Add(x => new TestCommand3(x.Item1, x.Item2, "Again"))
.Add(x => new TestCommand2(x.Value, "World"))
.Add(x => new TestCommand3(x.Value.Item1, x.Value.Item2, "Again"))
.SendAsync();

Console.WriteLine(result2);
}
}

public record TestCommand1(string Param1) : IRequest<string>;
public record TestCommand2(string Param1, string Param2) : IRequest<(string, string)>;
public record TestCommand3(string Param1, string Param2, string Param3) : IRequest<(string, string, string)>;
public record TestCommand1(string Param1) : IRequest<ErrorOr<string>>;
public record TestCommand2(string Param1, string Param2) : IRequest<ErrorOr<(string, string)>>;
public record TestCommand3(string Param1, string Param2, string Param3) : IRequest<ErrorOr<(string, string, string)>>;

internal class Command1Handler : IRequestHandler<TestCommand1, string>
internal class Command1Handler : IRequestHandler<TestCommand1, ErrorOr<string>>
{
public Task<string> Handle(TestCommand1 request, CancellationToken cancellationToken)
public async Task<ErrorOr<string>> Handle(TestCommand1 request, CancellationToken cancellationToken)
{
Console.WriteLine($"Command1Handler: {request.Param1}");
return Task.FromResult(request.Param1);
return await Task.FromResult(Error.Failure("test"));
}
}

internal class Command2Handler : IRequestHandler<TestCommand2, (string, string)>
internal class Command2Handler : IRequestHandler<TestCommand2, ErrorOr<(string, string)>>
{
public Task<(string, string)> Handle(TestCommand2 request, CancellationToken cancellationToken)
public async Task<ErrorOr<(string, string)>> Handle(TestCommand2 request, CancellationToken cancellationToken)
{
Console.WriteLine($"Command2Handler: {request.Param1}, {request.Param2}");
return Task.FromResult((request.Param1, request.Param2));
return await Task.FromResult((request.Param1, request.Param2));
}
}

internal class Command3Handler : IRequestHandler<TestCommand3, (string, string, string)>
internal class Command3Handler : IRequestHandler<TestCommand3, ErrorOr<(string, string, string)>>
{
public Task<(string, string, string)> Handle(TestCommand3 request, CancellationToken cancellationToken)
public async Task<ErrorOr<(string, string, string)>> Handle(TestCommand3 request, CancellationToken cancellationToken)
{
Console.WriteLine($"Command3Handler: {request.Param1}, {request.Param2}, {request.Param3}");
return Task.FromResult((request.Param1, request.Param2, request.Param3));
return await Task.FromResult((request.Param1, request.Param2, request.Param3));
}
}
6 changes: 5 additions & 1 deletion MediatR.Chained.Console/MediatR.Chained.Console.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="MediatR.Chained.EntityFrameworkCore" Version="1.0.2" />
<PackageReference Include="ErrorOr" Version="2.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\MediatR.Chained.EntityFrameworkCore\MediatR.Chained.EntityFrameworkCore.csproj" />
</ItemGroup>

</Project>
38 changes: 21 additions & 17 deletions MediatR.Chained.EntityFrameworkCore/MediatorExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,22 @@ namespace MediatR.Chained;
public static class MediatorExtensions
{
/// <summary>
/// Sends the specified mediator chain asynchronously within the context of a database transaction.
/// Sends a request through the mediator chain within the specified database transaction.
/// </summary>
/// <param name="mediatorChain">The mediator chain to send.</param>
/// <typeparam name="TResponse">The type of the response.</typeparam>
/// <param name="mediatorChain">The mediator chain.</param>
/// <param name="transaction">The database transaction.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task representing the asynchronous operation.</returns>
public static async Task SendAsync(this IMediatorChain mediatorChain, IDbContextTransaction transaction, CancellationToken cancellationToken = default)
public static async Task SendAsync<TResponse>(this IMediatorChain mediatorChain, IDbContextTransaction transaction, CancellationToken cancellationToken = default)
{
string savePointName = $"{nameof(IMediatorChain)}_{Guid.NewGuid()}";

await transaction.CreateSavepointAsync(savePointName, cancellationToken);

try
{
await mediatorChain.SendAsync(cancellationToken);
await mediatorChain.SendAsync<TResponse>(cancellationToken);
}
catch
{
Expand All @@ -33,36 +34,39 @@ public static async Task SendAsync(this IMediatorChain mediatorChain, IDbContext
}

/// <summary>
/// Sends the specified mediator chain asynchronously within the context of a database facade.
/// Sends a request through the mediator chain within a database transaction created from the specified database facade.
/// </summary>
/// <param name="mediatorChain">The mediator chain to send.</param>
/// <typeparam name="TResponse">The type of the response.</typeparam>
/// <param name="mediatorChain">The mediator chain.</param>
/// <param name="databaseFacade">The database facade.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task representing the asynchronous operation.</returns>
public static async Task SendAsync(this IMediatorChain mediatorChain, DatabaseFacade databaseFacade, CancellationToken cancellationToken = default)
public static async Task SendAsync<TResponse>(this IMediatorChain mediatorChain, DatabaseFacade databaseFacade, CancellationToken cancellationToken = default)
{
using IDbContextTransaction transaction = databaseFacade.BeginTransaction();
await SendAsync(mediatorChain, transaction, cancellationToken);
await SendAsync<TResponse>(mediatorChain, transaction, cancellationToken);
}

/// <summary>
/// Sends the specified mediator chain asynchronously within the context of a DbContext.
/// Sends a request through the mediator chain within a database transaction created from the specified DbContext.
/// </summary>
/// <param name="mediatorChain">The mediator chain to send.</param>
/// <typeparam name="TResponse">The type of the response.</typeparam>
/// <param name="mediatorChain">The mediator chain.</param>
/// <param name="dbContext">The DbContext.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task representing the asynchronous operation.</returns>
public static async Task SendAsync(this IMediatorChain mediatorChain, DbContext dbContext, CancellationToken cancellationToken = default)
=> await mediatorChain.SendAsync(dbContext.Database, cancellationToken: cancellationToken);
public static async Task SendAsync<TResponse>(this IMediatorChain mediatorChain, DbContext dbContext, CancellationToken cancellationToken = default)
=> await mediatorChain.SendAsync<TResponse>(dbContext.Database, cancellationToken: cancellationToken);

/// <summary>
/// Sends the specified mediator chain asynchronously within the context of a DbContextFactory.
/// Sends a request through the mediator chain within a database transaction created from the specified IDbContextFactory.
/// </summary>
/// <typeparam name="TResponse">The type of the response.</typeparam>
/// <typeparam name="TDbContext">The type of the DbContext.</typeparam>
/// <param name="mediatorChain">The mediator chain to send.</param>
/// <param name="factory">The DbContextFactory.</param>
/// <param name="mediatorChain">The mediator chain.</param>
/// <param name="factory">The IDbContextFactory.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task representing the asynchronous operation.</returns>
public static async Task SendAsync<TDbContext>(this IMediatorChain mediatorChain, IDbContextFactory<TDbContext> factory, CancellationToken cancellationToken = default) where TDbContext : DbContext
=> await mediatorChain.SendAsync((await factory.CreateDbContextAsync(cancellationToken)).Database, cancellationToken);
public static async Task SendAsync<TResponse, TDbContext>(this IMediatorChain mediatorChain, IDbContextFactory<TDbContext> factory, CancellationToken cancellationToken = default) where TDbContext : DbContext
=> await mediatorChain.SendAsync<TResponse>((await factory.CreateDbContextAsync(cancellationToken)).Database, cancellationToken);
}
150 changes: 100 additions & 50 deletions MediatR.Chained.Tests/MediatorChainTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,94 +7,144 @@ namespace MediatR.Chained.Tests;
public class MediatorChainTests
{
[Fact]
public async Task SendAsync_ShouldExecuteAllStepsInOrder()
public void MediatorChain_AddRequest_ShouldAddRequestToChain()
{
// Arrange
Mock<IMediator> mediatorMock = new();
List<Func<object, Task<object?>>> steps = [];
List<MediatorChainStep> steps = [];
MediatorChain chain = new(mediatorMock.Object, steps);

bool step1Executed = false;
bool step2Executed = false;
bool step3Executed = false;
SampleRequest1 request = new();

steps.Add(_ =>
{
step1Executed = true;
return Task.FromResult<object>(null!)!;
});
// Act
IMediatorChain<SampleResponse1> nextChain = chain.Add(request);

steps.Add(_ =>
{
step2Executed = true;
return Task.FromResult<object>(null!)!;
});
// Assert
steps.Should().HaveCount(1);
steps[0].Type.Should().Be(MediatorChainStep.StepType.Add);
steps[0].Predicate.Invoke(null!).Should().Be(request);
nextChain.Should().BeOfType<MediatorChain<SampleResponse1>>();
}

steps.Add(_ =>
{
step3Executed = true;
return Task.FromResult<object>(null!)!;
});
[Fact]
public void MediatorChain_AddRequestWithPreviousResult_ShouldAddRequestToChainWithPreviousResult()
{
// Arrange
Mock<IMediator> mediatorMock = new();
List<MediatorChainStep> steps = [];
MediatorChain chain = new(mediatorMock.Object, steps);

SampleRequest1 request1 = new();
SampleRequest2 request2 = new();
SampleResponse1 previousResult = new();

// Act
await chain.SendAsync();
IMediatorChain<SampleResponse1> previouseChain = chain.Add(request1);

IMediatorChain<SampleResponse2> nextChain = previouseChain.Add(request2);

// Assert
step1Executed.Should().BeTrue();
step2Executed.Should().BeTrue();
step3Executed.Should().BeTrue();
steps.Should().HaveCount(2);
steps[0].Type.Should().Be(MediatorChainStep.StepType.Add);
steps[1].Predicate(previousResult).Should().Be(request2);
nextChain.Should().BeOfType<MediatorChain<SampleResponse2>>();
}

[Fact]
public async Task Add_ShouldAddStepToChain()
public async Task MediatorChain_SendAsync_ShouldSendChainOfRequestsAndReturnResponse()
{
// Arrange
Mock<IMediator> mediatorMock = new();
List<Func<object, Task<object?>>> steps = [];
List<MediatorChainStep> steps = [];
MediatorChain chain = new(mediatorMock.Object, steps);

TestRequest request = new();
SampleRequest1 request1 = new();
SampleRequest2 request2 = new();
SampleResponse1 response1 = new();
SampleResponse2 response2 = new();

// Act
IMediatorChain<TestResponse> nextChain = chain.Add(request);
mediatorMock.Setup(x => x.Send((object)request1, It.IsAny<CancellationToken>()))
.ReturnsAsync(response1);

// Assert
steps.Should().HaveCount(1);
steps[0].Should().NotBeNull();
steps[0].Should().BeOfType<Func<object, Task<object?>>>();
mediatorMock.Setup(x => x.Send((object)request2, It.IsAny<CancellationToken>()))
.ReturnsAsync(response2);

chain.Add(request1);
chain.Add(request2);

object? result = await steps[0](null!);
result.Should().BeNull();
// Act
SampleResponse2? response = await chain.SendAsync<SampleResponse2>();

nextChain.Should().NotBeNull();
nextChain.Should().BeOfType<MediatorChain<TestResponse>>();
// Assert
response.Should().Be(response2);
mediatorMock.Verify(x => x.Send((object)request1, It.IsAny<CancellationToken>()), Times.Once);
mediatorMock.Verify(x => x.Send((object)request2, It.IsAny<CancellationToken>()), Times.Once);
}

[Fact]
public async Task AddRequest_ShouldAddStepToChain()
public async Task MediatorChain_SendAsync_WithCancellation_ShouldSendChainOfRequestsAndReturnResponse()
{
// Arrange
Mock<IMediator> mediatorMock = new();
List<Func<object, Task<object?>>> steps = [];
List<MediatorChainStep> steps = [];
MediatorChain chain = new(mediatorMock.Object, steps);

TestRequest request = new();
SampleRequest1 request1 = new();
SampleRequest2 request2 = new();
SampleResponse1 response1 = new();
SampleResponse2 response2 = new();

mediatorMock.Setup(x => x.Send((object)request1, It.IsAny<CancellationToken>()))
.ReturnsAsync(response1);

mediatorMock.Setup(x => x.Send((object)request2, It.IsAny<CancellationToken>()))
.ReturnsAsync(response2);

chain.Add(request1);
chain.Add(request2);

CancellationToken cancellationToken = new();

// Act
IMediatorChain<TestResponse> nextChain = chain.Add<TestRequest, TestResponse>(prevResult => new TestRequest());
SampleResponse2? response = await chain.SendAsync<SampleResponse2>(cancellationToken);

// Assert
steps.Should().HaveCount(1);
steps[0].Should().NotBeNull();
steps[0].Should().BeOfType<Func<object, Task<object?>>>();
response.Should().Be(response2);
mediatorMock.Verify(x => x.Send((object)request1, cancellationToken), Times.Once);
mediatorMock.Verify(x => x.Send((object)request2, cancellationToken), Times.Once);
}

object? result = await steps[0](null!);
result.Should().BeNull();
[Fact]
public async Task MediatorChain_SendAsync_WithFailWhenCondition_ShouldStopChainExecution()
{
// Arrange
Mock<IMediator> mediatorMock = new();

SampleRequest1 request1 = new();
SampleRequest2 request2 = new();
SampleResponse1 response1 = new();
SampleResponse2 response2 = new();

mediatorMock.Setup(x => x.Send((object)request1, It.IsAny<CancellationToken>()))
.ReturnsAsync(response1);

IMediatorChain<SampleResponse2> chain = mediatorMock.Object
.Add(request1)
.FailWhen(_ => true)
.Add(request2);

nextChain.Should().NotBeNull();
nextChain.Should().BeOfType<MediatorChain<TestResponse>>();
// Act
SampleResponse1? response = await chain.SendAsync<SampleResponse1>();

// Assert
response.Should().Be(response1);
mediatorMock.Verify(x => x.Send((object)request1, It.IsAny<CancellationToken>()), Times.Once);
mediatorMock.Verify(x => x.Send((object)request2, It.IsAny<CancellationToken>()), Times.Never);
}

private class TestRequest : IRequest<TestResponse>;
private class TestResponse;
private class SampleRequest1 : IRequest<SampleResponse1>;
private class SampleRequest2 : IRequest<SampleResponse2>;

private class SampleResponse1;
private class SampleResponse2;
}
Loading

0 comments on commit 5ecd2cf

Please sign in to comment.