Skip to content

Commit

Permalink
Wrapped FluentValidation with generic ValidationService and then appl…
Browse files Browse the repository at this point in the history
…ied it to QueryBus and CommandBus so that we can tie in validation to CQRS.
  • Loading branch information
jasonmwebb-lv committed May 6, 2024
1 parent 6fe03d8 commit ea89198
Show file tree
Hide file tree
Showing 28 changed files with 781 additions and 50 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

<ItemGroup>
<ProjectReference Include="..\..\..\Src\RCommon.ApplicationServices\RCommon.ApplicationServices.csproj" />
<ProjectReference Include="..\..\..\Src\RCommon.FluentValidation\RCommon.FluentValidation.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
using Examples.ApplicationServices.CQRS;
using Examples.ApplicationServices.CQRS.Validators;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using RCommon;
using RCommon.ApplicationServices;
using RCommon.ApplicationServices.ExecutionResults;
using RCommon.FluentValidation;
using System.Diagnostics;

try
Expand All @@ -20,20 +22,29 @@
{
// Configure RCommon
services.AddRCommon()
.WithCQRS<CqrsBuilder>(builder =>
.WithCQRS<CqrsBuilder>(cqrs =>
{
builder.AddQueryHandler<TestQueryHandler, TestQuery, TestDto>();
builder.AddCommandHandler<TestCommandHandler, TestCommand, IExecutionResult>();
});
cqrs.AddQueryHandler<TestQueryHandler, TestQuery, TestDto>();
cqrs.AddCommandHandler<TestCommandHandler, TestCommand, IExecutionResult>();
})
.WithValidation<FluentValidationBuilder>(validation =>
{
validation.AddValidatorsFromAssemblyContaining(typeof(TestCommand));

validation.UseWithCqrs(options =>
{
options.ValidateCommands = true;
options.ValidateQueries = true;
});
});
Console.WriteLine(services.GenerateServiceDescriptorsString());
services.AddTransient<ITestApplicationService, TestApplicationService>();

}).Build();

Console.WriteLine("Example Starting");

var appService = host.Services.GetRequiredService<ITestApplicationService>();
var commandResult = await appService.ExecuteTestCommand(new TestCommand());
var commandResult = await appService.ExecuteTestCommand(new TestCommand("test"));
var queryResult = await appService.ExecuteTestQuery(new TestQuery());

Console.WriteLine(commandResult.ToString());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,11 @@ namespace Examples.ApplicationServices.CQRS
{
public class TestCommand : ICommand<IExecutionResult>
{
public TestCommand(string message)
{
Message = message;
}

public string Message { get; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using FluentValidation;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Examples.ApplicationServices.CQRS.Validators
{
public class TestCommandValidator : AbstractValidator<TestCommand>
{
public TestCommandValidator()
{
RuleFor(command => command.Message).NotNull().NotEmpty();
}
}
}
7 changes: 7 additions & 0 deletions Examples/Examples.sln
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Examples.Mediator.MediatR",
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Mediator", "Mediator", "{9085B4F5-E26A-471D-B25D-5D69B0337B02}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RCommon.FluentValidation", "..\Src\RCommon.FluentValidation\RCommon.FluentValidation.csproj", "{BBBCCC2B-2218-4C32-96EE-C2153A23F643}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -303,6 +305,10 @@ Global
{05B6EF05-8053-45D1-8649-1779B0D7D6C7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{05B6EF05-8053-45D1-8649-1779B0D7D6C7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{05B6EF05-8053-45D1-8649-1779B0D7D6C7}.Release|Any CPU.Build.0 = Release|Any CPU
{BBBCCC2B-2218-4C32-96EE-C2153A23F643}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BBBCCC2B-2218-4C32-96EE-C2153A23F643}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BBBCCC2B-2218-4C32-96EE-C2153A23F643}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BBBCCC2B-2218-4C32-96EE-C2153A23F643}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -352,6 +358,7 @@ Global
{06FE76EC-C7D1-4C46-8047-22D58C8437A2} = {35AE0870-0A6D-4F27-B534-B8DCDFD11A36}
{05B6EF05-8053-45D1-8649-1779B0D7D6C7} = {9085B4F5-E26A-471D-B25D-5D69B0337B02}
{9085B4F5-E26A-471D-B25D-5D69B0337B02} = {3234C3BB-1632-4684-838E-9D6D382D4D4D}
{BBBCCC2B-2218-4C32-96EE-C2153A23F643} = {3199F749-0082-41D0-91D3-ECED117F8B08}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {0B0CD26D-8067-4667-863E-6B0EE7EDAA42}
Expand Down
18 changes: 16 additions & 2 deletions Src/RCommon.ApplicationServices/Commands/CommandBus.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Security.Principal;
Expand All @@ -30,9 +31,11 @@
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using RCommon.ApplicationServices.Caching;
using RCommon.ApplicationServices.Commands;
using RCommon.ApplicationServices.ExecutionResults;
using RCommon.ApplicationServices.Validation;
using RCommon.Reflection;

namespace RCommon.ApplicationServices.Commands
Expand All @@ -42,19 +45,30 @@ public class CommandBus : ICommandBus
private readonly ILogger<CommandBus> _logger;
private readonly IServiceProvider _serviceProvider;
private readonly IMemoryCache _memoryCache;
private readonly IValidationService _validationService;
private readonly IOptions<CqrsValidationOptions> _validationOptions;

public CommandBus(ILogger<CommandBus> logger, IServiceProvider serviceProvider, IMemoryCache memoryCache)
public CommandBus(ILogger<CommandBus> logger, IServiceProvider serviceProvider, IMemoryCache memoryCache, IValidationService validationService,
IOptions<CqrsValidationOptions> validationOptions)
{
_logger = logger;
_serviceProvider = serviceProvider;
_memoryCache = memoryCache;
_validationService = validationService;
_validationOptions = validationOptions;
}

public async Task<TResult> DispatchCommandAsync<TResult>(ICommand<TResult> command, CancellationToken cancellationToken)
public async Task<TResult> DispatchCommandAsync<TResult>(ICommand<TResult> command, CancellationToken cancellationToken = default)
where TResult : IExecutionResult
{
if (command == null) throw new ArgumentNullException(nameof(command));

if (_validationOptions.Value != null && _validationOptions.Value.ValidateCommands)
{
// TODO: Would be nice to be able to take validation outcome and put in FailedExecutionResult. Need some casting magic
await _validationService.ValidateAsync(command, true, cancellationToken);
}

if (_logger.IsEnabled(LogLevel.Trace))
{
_logger.LogTrace(
Expand Down
3 changes: 2 additions & 1 deletion Src/RCommon.ApplicationServices/Commands/ICommandBus.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ namespace RCommon.ApplicationServices.Commands
{
public interface ICommandBus
{
Task<TResult> DispatchCommandAsync<TResult>(ICommand<TResult> command, CancellationToken cancellationToken) where TResult : IExecutionResult;
Task<TResult> DispatchCommandAsync<TResult>(ICommand<TResult> command, CancellationToken cancellationToken = default)
where TResult : IExecutionResult;
}
}
21 changes: 21 additions & 0 deletions Src/RCommon.ApplicationServices/CqrsValidationOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace RCommon.ApplicationServices
{
public class CqrsValidationOptions
{
public CqrsValidationOptions()
{
ValidateQueries = false;
ValidateCommands = false;
}

public bool ValidateQueries { get; set; }
public bool ValidateCommands { get; set; }
}
}
14 changes: 14 additions & 0 deletions Src/RCommon.ApplicationServices/IValidationBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace RCommon.ApplicationServices
{
public interface IValidationBuilder
{
IServiceCollection Services { get; }
}
}
2 changes: 1 addition & 1 deletion Src/RCommon.ApplicationServices/Queries/IQueryBus.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ namespace RCommon.ApplicationServices.Queries
{
public interface IQueryBus
{
Task<TResult> DispatchQueryAsync<TResult>(IQuery<TResult> query, CancellationToken cancellationToken);
Task<TResult> DispatchQueryAsync<TResult>(IQuery<TResult> query, CancellationToken cancellationToken = default);
}
}
17 changes: 15 additions & 2 deletions Src/RCommon.ApplicationServices/Queries/QueryBus.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using RCommon.ApplicationServices.Caching;
using RCommon.ApplicationServices.Validation;
using RCommon.Reflection;

namespace RCommon.ApplicationServices.Queries
Expand All @@ -45,16 +47,27 @@ private class CacheItem
private readonly ILogger<QueryBus> _logger;
private readonly IServiceProvider _serviceProvider;
private readonly IMemoryCache _memoryCache;
private readonly IValidationService _validationService;
private readonly IOptions<CqrsValidationOptions> _validationOptions;

public QueryBus(ILogger<QueryBus> logger, IServiceProvider serviceProvider, IMemoryCache memoryCache)
public QueryBus(ILogger<QueryBus> logger, IServiceProvider serviceProvider, IMemoryCache memoryCache, IValidationService validationService,
IOptions<CqrsValidationOptions> validationOptions)
{
_logger = logger;
_serviceProvider = serviceProvider;
_memoryCache = memoryCache;
_validationService = validationService;
_validationOptions = validationOptions;
}

public async Task<TResult> DispatchQueryAsync<TResult>(IQuery<TResult> query, CancellationToken cancellationToken)
public async Task<TResult> DispatchQueryAsync<TResult>(IQuery<TResult> query, CancellationToken cancellationToken = default)
{
if (_validationOptions.Value != null && _validationOptions.Value.ValidateQueries)
{
// TODO: Would be nice to be able to take validation outcome and put in IQuery. Need some casting magic
await _validationService.ValidateAsync(query, true, cancellationToken);
}

var queryType = query.GetType();
var cacheItem = GetCacheItem(queryType);

Expand Down
16 changes: 16 additions & 0 deletions Src/RCommon.ApplicationServices/Validation/IValidationProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace RCommon.ApplicationServices.Validation
{
public interface IValidationProvider
{
Task<ValidationOutcome> ValidateAsync<T>(T target, bool throwOnFaults, CancellationToken cancellationToken = default)
where T : class;

}
}
10 changes: 10 additions & 0 deletions Src/RCommon.ApplicationServices/Validation/IValidationService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Threading;
using System.Threading.Tasks;

namespace RCommon.ApplicationServices.Validation
{
public interface IValidationService
{
Task<ValidationOutcome> ValidateAsync<T>(T target, bool throwOnFaults = false, CancellationToken cancellationToken = default) where T : class;
}
}
27 changes: 27 additions & 0 deletions Src/RCommon.ApplicationServices/Validation/Severity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace RCommon.ApplicationServices.Validation
{
/// <summary>
/// Specifies the severity of a rule.
/// </summary>
public enum Severity
{
/// <summary>
/// Error
/// </summary>
Error,
/// <summary>
/// Warning
/// </summary>
Warning,
/// <summary>
/// Info
/// </summary>
Info
}
}
Loading

0 comments on commit ea89198

Please sign in to comment.