Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
0c392d6
add first test
hasanxdev Jul 28, 2025
592a795
✨ Add unit tests for DispatchR configuration and handler registration
hasanxdev Jul 29, 2025
0110122
✨ Add tests for handler inclusion/exclusion and exceptions for empty …
hasanxdev Aug 2, 2025
929f47e
✨ Add unit test for generic pipeline registration in DispatchR config…
hasanxdev Aug 2, 2025
1f10490
✨ Add StreamRequestHandler tests and update Fixture with stream reque…
hasanxdev Aug 2, 2025
6ded974
✨ Rename tests for clarity and add notification registration tests
hasanxdev Aug 2, 2025
dc58d52
✨ Add unit tests for new pipeline behaviors and update service regist…
hasanxdev Aug 3, 2025
b9b20c6
✨ Update build-release.yml to conditionally execute steps for version…
hasanxdev Aug 3, 2025
c69ac25
✨ Update build-release.yml to include coverage report file in Codecov…
hasanxdev Aug 3, 2025
664d86a
✨ Update Codecov action to version 5 and adjust token handling
hasanxdev Aug 3, 2025
631424f
✨ Update build-release.yml to specify coverage report file for Codeco…
hasanxdev Aug 3, 2025
d42d4b9
✨ Update build-release.yml to change coverage report file path for Co…
hasanxdev Aug 3, 2025
aa9cd35
✨ Update build-release.yml to ensure consistent code coverage collect…
hasanxdev Aug 3, 2025
ab60598
✨ Update build-release.yml to adjust coverage report file path for Co…
hasanxdev Aug 3, 2025
3c7c8d0
✨ Update build-release.yml to consolidate unit and integration tests …
hasanxdev Aug 3, 2025
6b6ad0c
✨ Update build-release.yml to remove redundant coverage report file s…
hasanxdev Aug 3, 2025
50a2cf1
✨ Update build-release.yml to add a step for listing files before upl…
hasanxdev Aug 3, 2025
ee9cba3
✨ Update build-release.yml to list files in the tests directory befor…
hasanxdev Aug 3, 2025
2c044e8
✨ Update test project files to upgrade coverlet packages to version 6…
hasanxdev Aug 3, 2025
9022fe0
✨ Update build-release.yml to list files in the DispatchR.Integration…
hasanxdev Aug 3, 2025
6e285ef
✨ Update build-release.yml to install Coverlet tool and modify test c…
hasanxdev Aug 3, 2025
1b72bfa
✨ Update build-release.yml to remove unnecessary build step from test…
hasanxdev Aug 3, 2025
31b4fd3
✨ Update README.md to add Codecov badge for coverage tracking
hasanxdev Aug 3, 2025
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
20 changes: 20 additions & 0 deletions .github/workflows/build-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ on:
push:
tags:
- '*.*.*'
pull_request:
branches:
- main

jobs:
build:
Expand All @@ -23,18 +26,35 @@ jobs:
with:
dotnet-version: '9.x'

- name: Install Coverlet
run: dotnet tool install --global coverlet.console

- name: Restore
run: dotnet restore src/DispatchR/DispatchR.csproj

- name: Build
run: dotnet build src/DispatchR/DispatchR.csproj --configuration Release --no-restore

- name: Run Tests
run: dotnet test --collect:"XPlat Code Coverage"

- name: List files
run: ls -alh tests/DispatchR.IntegrationTest

- name: Upload Coverage to Codecov
uses: codecov/codecov-action@v5
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

- name: Extract version from tag
id: get_version
if: startsWith(github.ref, 'refs/tags/v')
run: echo "version=${GITHUB_REF#refs/tags/v}" >> "$GITHUB_OUTPUT"

- name: Pack project
if: startsWith(github.ref, 'refs/tags/v')
run: dotnet pack src/DispatchR/DispatchR.csproj --configuration Release --no-build -o ./nupkgs /p:PackageVersion=${{ steps.get_version.outputs.version }}

- name: Push to NuGet
if: startsWith(github.ref, 'refs/tags/v')
run: dotnet nuget push "./nupkgs/*.nupkg" --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json
45 changes: 45 additions & 0 deletions DispatchR.sln
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspireModularSample.Service
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspireModularSample.ServiceB", "src\AspireModularExample\AspireModularSample.ServiceB\AspireModularSample.ServiceB.csproj", "{707E07BA-998C-49DE-BA56-7E9C0B6B7DBA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DispatchR.UnitTest", "tests\DispatchR.UnitTest\DispatchR.UnitTest.csproj", "{806030F5-86B1-4EFC-923C-94FF7D32DFC9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DispatchR.IntegrationTest", "tests\DispatchR.IntegrationTest\DispatchR.IntegrationTest.csproj", "{D8646A62-9FE7-4E79-861C-49391007F98A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DispatchR.TestCommon", "tests\DispatchR.TestCommon\DispatchR.TestCommon.csproj", "{F01B6563-64D0-4316-947C-AB75426D9924}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -131,6 +137,42 @@ Global
{707E07BA-998C-49DE-BA56-7E9C0B6B7DBA}.Release|x64.Build.0 = Release|Any CPU
{707E07BA-998C-49DE-BA56-7E9C0B6B7DBA}.Release|x86.ActiveCfg = Release|Any CPU
{707E07BA-998C-49DE-BA56-7E9C0B6B7DBA}.Release|x86.Build.0 = Release|Any CPU
{806030F5-86B1-4EFC-923C-94FF7D32DFC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{806030F5-86B1-4EFC-923C-94FF7D32DFC9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{806030F5-86B1-4EFC-923C-94FF7D32DFC9}.Debug|x64.ActiveCfg = Debug|Any CPU
{806030F5-86B1-4EFC-923C-94FF7D32DFC9}.Debug|x64.Build.0 = Debug|Any CPU
{806030F5-86B1-4EFC-923C-94FF7D32DFC9}.Debug|x86.ActiveCfg = Debug|Any CPU
{806030F5-86B1-4EFC-923C-94FF7D32DFC9}.Debug|x86.Build.0 = Debug|Any CPU
{806030F5-86B1-4EFC-923C-94FF7D32DFC9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{806030F5-86B1-4EFC-923C-94FF7D32DFC9}.Release|Any CPU.Build.0 = Release|Any CPU
{806030F5-86B1-4EFC-923C-94FF7D32DFC9}.Release|x64.ActiveCfg = Release|Any CPU
{806030F5-86B1-4EFC-923C-94FF7D32DFC9}.Release|x64.Build.0 = Release|Any CPU
{806030F5-86B1-4EFC-923C-94FF7D32DFC9}.Release|x86.ActiveCfg = Release|Any CPU
{806030F5-86B1-4EFC-923C-94FF7D32DFC9}.Release|x86.Build.0 = Release|Any CPU
{D8646A62-9FE7-4E79-861C-49391007F98A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D8646A62-9FE7-4E79-861C-49391007F98A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D8646A62-9FE7-4E79-861C-49391007F98A}.Debug|x64.ActiveCfg = Debug|Any CPU
{D8646A62-9FE7-4E79-861C-49391007F98A}.Debug|x64.Build.0 = Debug|Any CPU
{D8646A62-9FE7-4E79-861C-49391007F98A}.Debug|x86.ActiveCfg = Debug|Any CPU
{D8646A62-9FE7-4E79-861C-49391007F98A}.Debug|x86.Build.0 = Debug|Any CPU
{D8646A62-9FE7-4E79-861C-49391007F98A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D8646A62-9FE7-4E79-861C-49391007F98A}.Release|Any CPU.Build.0 = Release|Any CPU
{D8646A62-9FE7-4E79-861C-49391007F98A}.Release|x64.ActiveCfg = Release|Any CPU
{D8646A62-9FE7-4E79-861C-49391007F98A}.Release|x64.Build.0 = Release|Any CPU
{D8646A62-9FE7-4E79-861C-49391007F98A}.Release|x86.ActiveCfg = Release|Any CPU
{D8646A62-9FE7-4E79-861C-49391007F98A}.Release|x86.Build.0 = Release|Any CPU
{F01B6563-64D0-4316-947C-AB75426D9924}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F01B6563-64D0-4316-947C-AB75426D9924}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F01B6563-64D0-4316-947C-AB75426D9924}.Debug|x64.ActiveCfg = Debug|Any CPU
{F01B6563-64D0-4316-947C-AB75426D9924}.Debug|x64.Build.0 = Debug|Any CPU
{F01B6563-64D0-4316-947C-AB75426D9924}.Debug|x86.ActiveCfg = Debug|Any CPU
{F01B6563-64D0-4316-947C-AB75426D9924}.Debug|x86.Build.0 = Debug|Any CPU
{F01B6563-64D0-4316-947C-AB75426D9924}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F01B6563-64D0-4316-947C-AB75426D9924}.Release|Any CPU.Build.0 = Release|Any CPU
{F01B6563-64D0-4316-947C-AB75426D9924}.Release|x64.ActiveCfg = Release|Any CPU
{F01B6563-64D0-4316-947C-AB75426D9924}.Release|x64.Build.0 = Release|Any CPU
{F01B6563-64D0-4316-947C-AB75426D9924}.Release|x86.ActiveCfg = Release|Any CPU
{F01B6563-64D0-4316-947C-AB75426D9924}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -146,5 +188,8 @@ Global
{3416F900-58F9-4AB6-AC8A-95B03C7BD9A3} = {BA3021C0-B64E-B700-D62A-004419E20C36}
{7D2890FF-66F7-4870-BB89-952167AB0681} = {BA3021C0-B64E-B700-D62A-004419E20C36}
{707E07BA-998C-49DE-BA56-7E9C0B6B7DBA} = {BA3021C0-B64E-B700-D62A-004419E20C36}
{806030F5-86B1-4EFC-923C-94FF7D32DFC9} = {7F7601D5-C62E-4EA3-8B71-E946A62B4529}
{D8646A62-9FE7-4E79-861C-49391007F98A} = {7F7601D5-C62E-4EA3-8B71-E946A62B4529}
{F01B6563-64D0-4316-947C-AB75426D9924} = {7F7601D5-C62E-4EA3-8B71-E946A62B4529}
EndGlobalSection
EndGlobal
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# DispatchR 🚀

![CI](https://github.com/hasanxdev/DispatchR/workflows/Release/badge.svg)
[![codecov](https://codecov.io/github/hasanxdev/dispatchr/graph/badge.svg?token=1FUG5DPUOE)](https://codecov.io/github/hasanxdev/dispatchr)
[![NuGet](https://img.shields.io/nuget/dt/DispatchR.Mediator.svg)](https://www.nuget.org/packages/DispatchR.Mediator)
[![NuGet](https://img.shields.io/nuget/vpre/DispatchR.Mediator.svg)](https://www.nuget.org/packages/DispatchR.Mediator)

Expand Down
52 changes: 27 additions & 25 deletions src/Benchmark/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.ComponentModel.DataAnnotations;
using Benchmark;
using Benchmark.Notification;
using Benchmark.SendRequest;
Expand All @@ -19,33 +18,36 @@
.AddColumn(new OperationsColumn())
);

public class OperationsColumn : IColumn
namespace Benchmark
{
public string Id => nameof(OperationsColumn);
public string ColumnName => "OpsCount";
public bool AlwaysShow => true;
public ColumnCategory Category => ColumnCategory.Custom;
public int PriorityInCategory => -10;
public bool IsNumeric => true;
public UnitType UnitType => UnitType.Dimensionless;
public string Legend => "Number of operations per invoke";

public string GetValue(Summary summary, BenchmarkCase benchmarkCase)
public class OperationsColumn : IColumn
{
return benchmarkCase.Descriptor.WorkloadMethod
.GetCustomAttributes(typeof(BenchmarkAttribute), false)
.Cast<BenchmarkAttribute>()
.FirstOrDefault()?.OperationsPerInvoke.ToString() ?? "1";
}
public string Id => nameof(OperationsColumn);
public string ColumnName => "OpsCount";
public bool AlwaysShow => true;
public ColumnCategory Category => ColumnCategory.Custom;
public int PriorityInCategory => -10;
public bool IsNumeric => true;
public UnitType UnitType => UnitType.Dimensionless;
public string Legend => "Number of operations per invoke";

public string GetValue(Summary summary, BenchmarkCase benchmarkCase, SummaryStyle style)
=> GetValue(summary, benchmarkCase);
public string GetValue(Summary summary, BenchmarkCase benchmarkCase)
{
return benchmarkCase.Descriptor.WorkloadMethod
.GetCustomAttributes(typeof(BenchmarkAttribute), false)
.Cast<BenchmarkAttribute>()
.FirstOrDefault()?.OperationsPerInvoke.ToString() ?? "1";
}

public bool IsDefault(Summary summary, BenchmarkCase benchmarkCase)
{
return true;
}
public string GetValue(Summary summary, BenchmarkCase benchmarkCase, SummaryStyle style)
=> GetValue(summary, benchmarkCase);

public bool IsDefault(Summary summary, BenchmarkCase benchmarkCase)
{
return true;
}

public bool IsAvailable(Summary summary) => true;
public bool IsDefault(Summary summary) => false;
public bool IsAvailable(Summary summary) => true;
public bool IsDefault(Summary summary) => false;
}
}
49 changes: 36 additions & 13 deletions src/DispatchR/Configuration/ServiceRegistrator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,22 @@ public static void RegisterHandlers(IServiceCollection services, List<Type> allT
.Where(p =>
{
var interfaces = p.GetInterfaces();
if (p.IsGenericType)
{
// handle generic pipelines
return interfaces
.FirstOrDefault(inter =>
inter.IsGenericType &&
inter.GetGenericTypeDefinition() == behaviorType)
?.GetInterfaces().First().GetGenericTypeDefinition() ==
handlerInterface.GetGenericTypeDefinition();
}

return interfaces
.FirstOrDefault(inter =>
inter.IsGenericType &&
inter.GetGenericTypeDefinition() == behaviorType)
?.GetInterfaces().First().GetGenericTypeDefinition() ==
handlerInterface.GetGenericTypeDefinition();
.FirstOrDefault(inter =>
inter.IsGenericType &&
inter.GetGenericTypeDefinition() == behaviorType)
?.GetInterfaces().First() == handlerInterface;
}).ToList();

// Sort pipelines by the specified order passed via ConfigurationOptions
Expand Down Expand Up @@ -92,17 +102,28 @@ public static void RegisterHandlers(IServiceCollection services, List<Type> allT
var responseTypeArg = handlerInterface.GenericTypeArguments[1];
if (genericHandlerResponseIsAwaitable && handlerResponseTypeIsAwaitable)
{
if (genericHandlerResponseType.GetGenericTypeDefinition() !=
handlerInterface.GenericTypeArguments[1].GetGenericTypeDefinition())
var areGenericTypeArgumentsInHandlerInterfaceMismatched =
genericHandlerResponseType.IsGenericType &&
handlerInterface.GenericTypeArguments[1].IsGenericType &&
genericHandlerResponseType.GetGenericTypeDefinition() !=
handlerInterface.GenericTypeArguments[1].GetGenericTypeDefinition();

if (areGenericTypeArgumentsInHandlerInterfaceMismatched ||
genericHandlerResponseType.IsGenericType ^
handlerInterface.GenericTypeArguments[1].IsGenericType)
{
continue;
}

// register async generic pipelines
responseTypeArg = responseTypeArg.GenericTypeArguments[0];
if (responseTypeArg.GenericTypeArguments.Any())
{
responseTypeArg = responseTypeArg.GenericTypeArguments[0];
}
}

var closedGenericType = pipeline.MakeGenericType(handlerInterface.GenericTypeArguments[0], responseTypeArg);
var closedGenericType = pipeline.MakeGenericType(handlerInterface.GenericTypeArguments[0],
responseTypeArg);
services.AddKeyedScoped(typeof(IRequestHandler), key, closedGenericType);
}
else
Expand Down Expand Up @@ -130,7 +151,8 @@ public static void RegisterHandlers(IServiceCollection services, List<Type> allT
}
}

public static void RegisterNotification(IServiceCollection services, List<Type> allTypes, Type syncNotificationHandlerType)
public static void RegisterNotification(IServiceCollection services, List<Type> allTypes,
Type syncNotificationHandlerType)
{
var allNotifications = allTypes
.Where(p =>
Expand All @@ -140,7 +162,7 @@ public static void RegisterNotification(IServiceCollection services, List<Type>
.Select(i => i.GetGenericTypeDefinition())
.Any(i => new[]
{
syncNotificationHandlerType
syncNotificationHandlerType
}.Contains(i));
})
.GroupBy(p =>
Expand All @@ -149,7 +171,7 @@ public static void RegisterNotification(IServiceCollection services, List<Type>
.Where(i => i.IsGenericType)
.First(i => new[]
{
syncNotificationHandlerType
syncNotificationHandlerType
}.Contains(i.GetGenericTypeDefinition()));
return @interface.GenericTypeArguments.First();
})
Expand All @@ -172,7 +194,8 @@ private static bool IsAwaitable(Type type)
if (type.IsGenericType)
{
var genericDef = type.GetGenericTypeDefinition();
return genericDef == typeof(Task<>) || genericDef == typeof(ValueTask<>) || genericDef == typeof(IAsyncEnumerable<>);
return genericDef == typeof(Task<>) || genericDef == typeof(ValueTask<>) ||
genericDef == typeof(IAsyncEnumerable<>);
}

return false;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
namespace DispatchR.Exceptions;

public class ExcludeHandlersCannotBeArrayEmptyException() : Exception("Exclude handlers cannot be array empty.")
{
}
7 changes: 7 additions & 0 deletions src/DispatchR/Exceptions/HandlerNotFoundException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace DispatchR.Exceptions;

public class HandlerNotFoundException<TRequest, TResponse>() : Exception(
$"""
Handler for request of type '{typeof(TRequest).Name}' returning '{typeof(TResponse).Name}' was not found.
Make sure you have registered a handler that implements IRequestHandler<{typeof(TRequest).Name}, {typeof(TResponse).Name}> in the DI container.
""");
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
namespace DispatchR.Exceptions;

public class IncludeHandlersCannotBeArrayEmptyException() : Exception("Include handlers cannot be array empty.")
{
}
11 changes: 11 additions & 0 deletions src/DispatchR/Extensions/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using DispatchR.Requests.Stream;
using Microsoft.Extensions.DependencyInjection;
using System.Reflection;
using DispatchR.Exceptions;

namespace DispatchR.Extensions;

Expand All @@ -15,6 +16,16 @@ public static IServiceCollection AddDispatchR(this IServiceCollection services,
var config = new ConfigurationOptions();
configuration(config);

if (config is {IncludeHandlers.Count:0})
{
throw new IncludeHandlersCannotBeArrayEmptyException();
}

if (config is {ExcludeHandlers.Count:0})
{
throw new ExcludeHandlersCannotBeArrayEmptyException();
}

return services.AddDispatchR(config);
}

Expand Down
12 changes: 10 additions & 2 deletions src/DispatchR/Requests/IMediator.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Runtime.CompilerServices;
using DispatchR.Exceptions;
using DispatchR.Requests.Notification;
using DispatchR.Requests.Send;
using DispatchR.Requests.Stream;
Expand All @@ -23,8 +24,15 @@ public sealed class Mediator(IServiceProvider serviceProvider) : IMediator
public TResponse Send<TRequest, TResponse>(IRequest<TRequest, TResponse> request,
CancellationToken cancellationToken) where TRequest : class, IRequest
{
return serviceProvider.GetRequiredService<IRequestHandler<TRequest, TResponse>>()
.Handle(Unsafe.As<TRequest>(request), cancellationToken);
try
{
return serviceProvider.GetRequiredService<IRequestHandler<TRequest, TResponse>>()
.Handle(Unsafe.As<TRequest>(request), cancellationToken);
}
catch (Exception e) when (e.Message.Contains("No service for type", StringComparison.OrdinalIgnoreCase))
{
throw new HandlerNotFoundException<TRequest, TResponse>();
}
}

public IAsyncEnumerable<TResponse> CreateStream<TRequest, TResponse>(IStreamRequest<TRequest, TResponse> request,
Expand Down
3 changes: 3 additions & 0 deletions src/DispatchR/Requests/Send/IRequestHandler.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
using System.Diagnostics.CodeAnalysis;

namespace DispatchR.Requests.Send;

public interface IRequestHandler
{
[ExcludeFromCodeCoverage]
internal void SetNext(object handler)
{
}
Expand Down
8 changes: 6 additions & 2 deletions src/Sample/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using DispatchR.Extensions;
using Sample;
using Scalar.AspNetCore;
using DispatchRNotificationSample = Sample.DispatchR.Notification;
using DispatchRSample = Sample.DispatchR.SendRequest;
Expand Down Expand Up @@ -125,7 +126,10 @@

app.Run();

record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
namespace Sample
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
}
Loading