Skip to content

Commit

Permalink
Added IRequestFilter with Behaviour and IResultFilter with Behaviour (#4
Browse files Browse the repository at this point in the history
)

* Added IRequestFilter with Behaviour to filter request of query or command.
* Added IResultFilter with Behaviour to filter result of query or command.
  • Loading branch information
denis-peshkov authored Apr 16, 2024
1 parent acc9fac commit ba6ee27
Show file tree
Hide file tree
Showing 16 changed files with 199 additions and 12 deletions.
7 changes: 4 additions & 3 deletions Cross.CQRS/Behaviors/EventQueueProcessBehavior.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
namespace Cross.CQRS.Behaviors;

internal sealed class EventQueueProcessBehavior<T, TR> : IPipelineBehavior<T, TR> where T : IRequest<TR>
internal sealed class EventQueueProcessBehavior<TRequest, TResult> : IPipelineBehavior<TRequest, TResult>
where TRequest : IRequest<TResult>
{
private readonly IEventQueueReader _eventReader;
private readonly IMediator _mediator;
Expand All @@ -12,11 +13,11 @@ public EventQueueProcessBehavior(IEventQueueReader eventReader, IMediator mediat
}

/// <inheritdoc />
public async Task<TR> Handle(T request, RequestHandlerDelegate<TR> next, CancellationToken cancellationToken)
public async Task<TResult> Handle(TRequest request, RequestHandlerDelegate<TResult> next, CancellationToken cancellationToken)
{
var result = await next();

if (request is ICommand<TR> identifiable)
if (request is ICommand<TResult> identifiable)
{
var events = _eventReader.Read(identifiable.CommandId);
foreach (var @event in events)
Expand Down
30 changes: 30 additions & 0 deletions Cross.CQRS/Behaviors/RequestFilterBehavior.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
namespace Cross.CQRS.Behaviors;

internal sealed class RequestFilterBehavior<TRequest, TResult> : IPipelineBehavior<TRequest, TResult>
where TRequest : IRequest<TResult>
{
private readonly IEnumerable<IRequestFilter<TRequest>> _filters;

public RequestFilterBehavior(IEnumerable<IRequestFilter<TRequest>> filters)
{
_filters = filters;
}

/// <inheritdoc />
public async Task<TResult> Handle(TRequest request, RequestHandlerDelegate<TResult> next, CancellationToken cancellationToken)
{
var filters = _filters.ToArray();
if (filters.Length <= 0)
{
return await next();
}

var result = request;
foreach (var filter in filters)
{
result = await filter.ApplyFilterAsync(result, cancellationToken);
}

return await next();
}
}
32 changes: 32 additions & 0 deletions Cross.CQRS/Behaviors/ResultFilterBehavior.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
namespace Cross.CQRS.Behaviors;

internal sealed class ResultFilterBehavior<TRequest, TResult> : IPipelineBehavior<TRequest, TResult>
where TRequest : IRequest<TResult>
{
private readonly IEnumerable<IResultFilter<TRequest, TResult>> _filters;

public ResultFilterBehavior(IEnumerable<IResultFilter<TRequest, TResult>> filters)
{
_filters = filters;
}

/// <inheritdoc />
public async Task<TResult> Handle(TRequest request, RequestHandlerDelegate<TResult> next, CancellationToken cancellationToken)
{
var filters = _filters.ToArray();
if (filters.Length <= 0)
{
return await next();
}

var responce = await next();

var result = responce;
foreach (var filter in filters)
{
result = await filter.ApplyFilterAsync(result, cancellationToken);
}

return result;
}
}
2 changes: 2 additions & 0 deletions Cross.CQRS/Cross.CQRS.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />

<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />

<PackageReference Include="Scrutor" Version="4.2.2" />
</ItemGroup>

</Project>
26 changes: 26 additions & 0 deletions Cross.CQRS/Filters/IRequestFilter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
namespace Cross.CQRS.Filters;

/// <summary>
/// Defines a filter for a particular type.
/// </summary>
/// <remarks>
/// Use it if you need to perform filter operation on the <see cref="TRequest"/>,
/// after the query <see cref="IQuery{TResponse}"/> or command <see cref="ICommand"/>.
/// </remarks>
public interface IRequestFilter<TRequest>
{
/// <summary>
/// Filter the specified instance.
/// </summary>
/// <param name="result">The instance to filter</param>
/// <returns>A ValidationResult object containing any validation failures.</returns>
TRequest ApplyFilter(TRequest result);

/// <summary>
/// Filter the specified instance asynchronously.
/// </summary>
/// <param name="result">The instance to filter</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>A ValidationResult object containing any validation failures.</returns>
Task<TRequest> ApplyFilterAsync(TRequest result, CancellationToken cancellationToken);
}
26 changes: 26 additions & 0 deletions Cross.CQRS/Filters/IResultFilter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
namespace Cross.CQRS.Filters;

/// <summary>
/// Defines a filter for a particular type.
/// </summary>
/// <remarks>
/// Use it if you need to perform filter operation on the <see cref="TResult"/>,
/// after the query <see cref="IQuery{TResponse}"/> or command <see cref="ICommand"/>.
/// </remarks>
public interface IResultFilter<TRequest, TResult>
{
/// <summary>
/// Filter the specified instance.
/// </summary>
/// <param name="result">The instance to filter</param>
/// <returns>A ValidationResult object containing any validation failures.</returns>
TResult ApplyFilter(TResult result);

/// <summary>
/// Filter the specified instance asynchronously.
/// </summary>
/// <param name="result">The instance to filter</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>A ValidationResult object containing any validation failures.</returns>
Task<TResult> ApplyFilterAsync(TResult result, CancellationToken cancellationToken);
}
15 changes: 15 additions & 0 deletions Cross.CQRS/Filters/RequestFilter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace Cross.CQRS.Filters;

/// <summary>
/// Base query/command filter.
/// </summary>
public abstract class RequestFilter<TRequest, TResult> : IRequestFilter<TRequest>
where TRequest : IRequest<TResult>
{
/// <inheritdoc />
public TRequest ApplyFilter(TRequest request)
=> ApplyFilterAsync(request, CancellationToken.None).GetAwaiter().GetResult();

/// <inheritdoc />
public abstract Task<TRequest> ApplyFilterAsync(TRequest request, CancellationToken cancellationToken);
}
15 changes: 15 additions & 0 deletions Cross.CQRS/Filters/ResultFilter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace Cross.CQRS.Filters;

/// <summary>
/// Base query/command filter.
/// </summary>
public abstract class ResultFilter<TRequest, TResult> : IResultFilter<TRequest, TResult>
where TRequest : IRequest<TResult>
{
/// <inheritdoc />
public TResult ApplyFilter(TResult result)
=> ApplyFilterAsync(result, CancellationToken.None).GetAwaiter().GetResult();

/// <inheritdoc />
public abstract Task<TResult> ApplyFilterAsync(TResult result, CancellationToken cancellationToken);
}
1 change: 1 addition & 0 deletions Cross.CQRS/GlobalUsings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
global using Cross.CQRS.Behaviors;
global using Cross.CQRS.Commands;
global using Cross.CQRS.Events;
global using Cross.CQRS.Filters;
global using Cross.CQRS.Queries;
global using Cross.CQRS.Services;
global using FluentValidation.Results;
15 changes: 14 additions & 1 deletion Cross.CQRS/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public static CqrsRegistrationSyntax AddCQRS(this IServiceCollection services, p
{
var behaviorCollection = new BehaviorCollection(services);

// FluentValidation
// FluentValidations
services.AddValidatorsFromAssembly(assemblies.FirstOrDefault(), ServiceLifetime.Scoped, result =>
{
var isNoRegisterAutomatically = result.ValidatorType
Expand All @@ -23,6 +23,17 @@ public static CqrsRegistrationSyntax AddCQRS(this IServiceCollection services, p
return !isNoRegisterAutomatically;
});

// Filters
services.Scan(scan => scan
.FromAssemblies(assemblies)
.AddClasses(classes => classes.AssignableTo(typeof(IResultFilter<,>)))
.AsImplementedInterfaces()
.WithScopedLifetime()
.AddClasses(classes => classes.AssignableTo(typeof(IRequestFilter<>)))
.AsImplementedInterfaces()
.WithScopedLifetime()
);

services.AddMediatR(o => o.AsScoped(), assemblies);

services.AddSingleton<IHandlerLocator>(_ => new HandlerLocator(services));
Expand All @@ -35,6 +46,8 @@ public static CqrsRegistrationSyntax AddCQRS(this IServiceCollection services, p
// Behaviors registered earlier will be executed earlier
behaviorCollection.AddBehavior(typeof(EventQueueProcessBehavior<,>), order: int.MinValue);
behaviorCollection.AddBehavior(typeof(ValidationBehavior<,>), order: 1);
behaviorCollection.AddBehavior(typeof(RequestFilterBehavior<,>), order: 2);
behaviorCollection.AddBehavior(typeof(ResultFilterBehavior<,>), order: 3);

return new CqrsRegistrationSyntax(services, assemblies, behaviorCollection);
}
Expand Down
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ Main Features:
Implemented base patterns to crate Events, approach how to write a new Events from the Commands, consuming patterns and behavior to handle it.
The main idea is to do some actions after the Commands have to be finished, to avoid cases when one Command call another one.

* **Filters**.

Here included filter behavior based on RequestFilter and ResultFilter.
The RequestFilter allow to filter Queries and Commands requests before their execution.
The ResultFilter allow to filter Queries and Commands results after their execution.

* **Validation**.

Here included validation behavior based on FluentValidation, that allow to validate Queries and Command before their execution.
Expand Down Expand Up @@ -54,5 +60,5 @@ Note - test project is not a part of nuget package. You have to clone repository
## Roadmap:
- ~~Queries implementation~~
- ~~Commands Implementation~~
- ~~Validation behavior~~ (based on FluentValidation)
- ~~Validation behavior (based on FluentValidation)~~
- ~~Events~~
5 changes: 5 additions & 0 deletions ReleaseNotes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,8 @@ Upgrade packages.
Added NoRegisterAutomaticallyAttribute to avoid register instance of class automatically in DI.
Small fixes.
Added sample project.

8.2.0 - 16 Apr 2024
Added IRequestFilter with Behaviour to filter request of query or command.
Added IResultFilter with Behaviour to filter result of query or command.

2 changes: 2 additions & 0 deletions SampleWebApp/GlobalUsings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
global using System.Collections.Generic;
global using System.Linq;
global using System.Text;
global using System.Threading;
global using System.Threading.Tasks;
global using Cross.CQRS;
global using Cross.CQRS.Behaviors;
global using Cross.CQRS.Filters;
global using Cross.CQRS.Queries;
global using FluentValidation;
global using Microsoft.AspNetCore.Builder;
Expand Down
11 changes: 11 additions & 0 deletions SampleWebApp/Modules/Some/Handlers/SomeResultFilter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace SampleWebApp.Modules.Some.Handlers;

public class SomeResultFilter : ResultFilter<SomeQuery, IEnumerable<string>>
{
public override Task<IEnumerable<string>> ApplyFilterAsync(IEnumerable<string> response, CancellationToken cancellationToken)
{
// do some filter actions

return Task.FromResult(response);
}
}
13 changes: 7 additions & 6 deletions _nuget/config.nuspec
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,30 @@
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
<metadata>
<id>Cross.CQRS</id>
<version>8.1.2</version>
<version>8.2.0</version>
<authors>denis-peshkov</authors>
<owners>denis-peshkov</owners>
<copyright>Copyright 2024</copyright>
<license type="expression">MIT</license>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<projectUrl>https://github.com/denis-peshkov/Cross.CQRS</projectUrl>
<repository type="git" url="https://github.com/denis-peshkov/Cross.CQRS" />
<readme>docs\README.md</readme>
<icon>icon.png</icon>
<releaseNotes>
Added NoRegisterAutomaticallyAttribute to avoid register instance of class automatically in DI.
Small fixes.
Added sample project.
Added IRequestFilter with Behaviour to filter request of query or command.
Added IResultFilter with Behaviour to filter result of query or command.
</releaseNotes>
<description>Simple .NET MediatR base Query, Command, Event. Event Queue and Validation behaviors. Written on C#.</description>
<tags>.NET Mediatr Query QueryHandler Command CommandHandler Event EventHandler EventQueueBehavior Validation ValidationBehavior SyntaxNotes NetCore</tags>
<description>Simple .NET MediatR base Query, Command, Event. RequestFilter, ResultFilter and Filter behaviours. Event Queue and Validation behaviors. Written on C#.</description>
<tags>.NET Mediatr Query QueryHandler Command CommandHandler Event EventHandler EventQueueBehavior Filter RequestFilter ResultFilter FilterBehavior Validation ValidationBehavior SyntaxNotes NetCore</tags>
<dependencies>
<dependency id="FluentValidation" version="11.8.1" />
<dependency id="FluentValidation.DependencyInjectionExtensions" version="11.8.1" />
<dependency id="MediatR" version="11.1.0" />
<dependency id="MediatR.Extensions.Microsoft.DependencyInjection" version="11.1.0" />
<dependency id="Microsoft.Extensions.Configuration" version="8.0.0" />
<dependency id="Microsoft.CSharp" version="4.7.0" />
<dependency id="Scrutor" version="4.2.2" />
</dependencies>
</metadata>
<files>
Expand Down
3 changes: 2 additions & 1 deletion _nuget/pkg.bat
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
dotnet build --configuration release ..\Cross.CQRS.sln
nuget.exe pack config.nuspec -Symbols -SymbolPackageFormat snupkg
REM nuget.exe pack config.nuspec -Symbols -SymbolPackageFormat snupkg
nuget.exe pack config.nuspec -Symbols

0 comments on commit ba6ee27

Please sign in to comment.