Masstransit v8 test consumer with TestServer - testing

I try to test MassTransit v8 consumer with TestServer by creating whole application with all DI services. I have created TestServer instance with replaced some dependencies but I am most concerned with adding MasstransitTestHarness with consumer.
In test message is sent and consumed by harness correctly, but I can't understand why my consumer can't consume message. TestStartup inherits from my application Startup when I have defined all DI services.
Here is an example of my code:
TestServer program
public class TestProgram
{
private const string API_URL = "http://localhost:5010";
public TestServer Server { get; set; }
public void SetUpTestServer()
{
var host = CreateWebHostBuilder();
Server = new TestServer(host);
}
private IWebHostBuilder CreateWebHostBuilder()
{
return new WebHostBuilder()
.ConfigureTestServices(serviceCollection =>
{
serviceCollection.AddMassTransitTestHarness(cfg =>
{
cfg.AddConsumer<MyConsumer>();
cfg.UsingInMemory((provider, config) =>
{
config.ReceiveEndpoint("MyMessageQueue",
e =>
{
e.Batch<IMyMessage>(b =>
{
b.Consumer<MyConsumer, IMyMessage>(provider);
});
});
});
});
})
.UseStartup<TestStartup>()
.UseUrls(API_URL)
.UseEnvironment("Test");
}
}
Single test
public async Task Test()
{
EndpointConvention.Map<IMyMessage>(new Uri($"queue:MyMessageQueue"));
var program = new TestProgram();
program.SetUpTestServer();
var harness = program.Server.Services.GetTestHarness();
await harness.Start();
try
{
await harness.Bus.Send<IMyMessage>(new MyMessage());
// It's OK
Assert.IsTrue(await harness.Sent.Any<IMyMessage>());
Assert.IsTrue(await harness.Consumed.Any<IMyMessage>());
var consumer = harness.GetConsumerHarness<MyConsumer>();
// It's wrong
Assert.That(await consumer.Consumed.Any<IMyMessage>());
}
finally
{
await harness.Stop();
}
}
Does anyone know why my consumer not consume sent message?

Your configuration is strange, you should follow the configuration guide:
private IWebHostBuilder CreateWebHostBuilder()
{
return new WebHostBuilder()
.ConfigureTestServices(serviceCollection =>
{
serviceCollection.AddMassTransitTestHarness(cfg =>
{
cfg.AddConsumer<MyConsumer>(c =>
c.Options<BatchOptions>(o => o.SetMessageLimit(5).SetTimeLimit(2000)))
.Endpoint(e => e.Name = "MyMessageQueue");;
});
})
.UseStartup<TestStartup>()
.UseUrls(API_URL)
.UseEnvironment("Test");
}
Also, the consumer doesn't consume IMyMessage, it consumes Batch<IMyMessage>, and I'm not entirely sure that consuming batch messages is visible in the harness, I'd have to check and be sure.
Also, as a complete test for comparison:
[TestFixture]
public class When_batch_limit_is_reached
{
[Test]
public async Task Should_deliver_the_batch_to_the_consumer()
{
await using var provider = new ServiceCollection()
.AddMassTransitTestHarness(x =>
{
x.AddConsumer<TestBatchConsumer>();
})
.BuildServiceProvider(true);
var harness = provider.GetTestHarness();
await harness.Start();
await harness.Bus.PublishBatch(new[] { new BatchItem(), new BatchItem() });
Assert.That(await harness.Consumed.SelectAsync<BatchItem>().Take(2).Count(), Is.EqualTo(2));
Assert.That(await harness.GetConsumerHarness<TestBatchConsumer>().Consumed.Any<Batch<BatchItem>>(), Is.True);
Assert.That(await harness.Published.Any<BatchResult>(x => x.Context.Message.Count == 2), Is.True);
}
}

Related

Masstransit and RabbitMQ to Console Application (use Asp.net Core Web API)

I created a Solution with ASP.NET Core WEB API project, some class libraries (Domain, DI and ect), and a console application.
A console application that I use as a RabbitMQ Consumer with Masstransit library it should take messages from RabbitMQ (I have Producer project and it sends for RabbitMQ messages without problems)
My ConsoleApplication:
like this Program.cs:
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
services.AddMassTransit(x =>
{
x.AddConsumer<MessageConsumer>();
x.UsingRabbitMq((context, cfg) =>
{
var connectionString = new Uri("RabbitMQ_URL");
cfg.Host(connectionString);
cfg.ConfigureEndpoints(context);
});
});
services.AddMassTransitHostedService(true);
services.AddHostedService<Worker>();
});
}
With Worker.cs:
public class Worker : BackgroundService
{
readonly IBus _bus;
public Worker(IBus bus)
{
_bus = bus;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var factory = new ConnectionFactory() { Uri = new
Uri("RabbitMQ_URL"), DispatchConsumersAsync = true };
using (var connection = factory.CreateConnection())
using (var channel = connection.CreateModel())
{
channel.QueueDeclare(queue: "MessageQueue",
durable: true,
exclusive: false,
autoDelete: false,
arguments: null);
var consumer = new AsyncEventingBasicConsumer(channel);
consumer.Received += async (model, ea) =>
{
var body = ea.Body.ToArray();
var message = Encoding.UTF8.GetString(body);
var #event = JsonConvert.DeserializeObject<Event>
(message);
await _bus.Publish(new Event { DataJson = #event });
await Task.Yield();
};
channel.BasicConsume(queue: "MessageQueue",
autoAck: true,
consumer: consumer);
_logger.LogInformation("Received Text: {Text}", context.Message.DataJson);
return Task.CompletedTask;
}
}
}
MessageConsumer.cs:
public class MessageConsumer :
IConsumer<Event>
{
readonly ILogger<MessageConsumer> _logger;
public MessageConsumer(ILogger<MessageConsumer> logger)
{
_logger = logger;
}
public Task Consume(ConsumeContext<Event> context)
{
_logger.LogInInformation("Recieved Text: {Text},
context.Message.DataJson");
return Task.CompletedTask;
}
}
And my Event.cs:
public class Event
{
public ServiceType ServiceType { get; set; }
public string DataJson { get; set; }
}
public enum ServiceType
{
ComplareSitter
}
Please help me
Thanks a lot.
You might start with a clean and simple worker service using one of the MassTransit templates, just to verify your setup/configuration. There is a video available showing how to setup and use the templates.
But an obvious question, why on earth are you connecting to RabbitMQ and creating a basic consumer inside the Consume method? The message has already been deserialized as your Event type and is ready to be used. There is absolutely no need to use any part of the RabbitMQ Client library in your application when using MassTransit.

How to test connection between asp net core application with Microsoft.AspNetCore.Mvc.Testing

I have multiple services and I want to use Microsoft.AspNetCore.Mvc.Testing to test connection between them. I use Microsoft.AspNetCore.Mvc.Testing for integration test for each service and I try to use it for multiple service scenario. But I run into issue how to do it
This is my test:
public class Web1ApplicationFactory : WebApplicationFactory<Startup1>
{
protected override IHostBuilder CreateHostBuilder() =>
Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup1>();
});
}
public class Web2ApplicationFactory : WebApplicationFactory<Startup2>
{
protected override IHostBuilder CreateHostBuilder() =>
Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup2>();
});
}
[Trait("Category", "E2E")]
public abstract class E2ETestsBase
{
protected readonly HttpClient client1;
protected readonly HttpClient client2;
protected E2ETestsBase()
{
client1 = new Web1ApplicationFactory().CreateClient();
client2 = new Web2ApplicationFactory().CreateClient();
}
}
public class CreateIncidentTests : E2ETestsBase
{
[Fact]
public async Task Should_PassFrom1TO2()
{
// Arrange
var id = MyId.New();
var body = new CreateRequest(id, "sample");
var requestContent = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
// Act
var response1 = await client1.PostAsync("Create", requestContent);
var response2 = await client2.GetAsync($"{id.IdValue}");
// Assert
Assert.Equal(HttpStatusCode.OK, response1.StatusCode);
Assert.Equal("application/json; charset=utf-8", response1.Content.Headers.ContentType?.ToString());
Assert.Equal($"\"{id.IdValue}\"", await response1.Content.ReadAsStringAsync());
Assert.Equal(HttpStatusCode.OK, response2.StatusCode);
}
}
Web1Application post data to Web2Application in some cases via HttpClient.PostAsync in development enviroment to https://localhost:156123. But how to post from Web1Application to Web2Application in tests? There is no port it runs in test process. Is there a way how to test scenario like this? Or I have to use selenium or something like that?

My AspnetCore application does not receive publications from a .net core console application via SignalR

My AspnetCore application does not receive publications from a .net core console application via SignalR
I have an aspnet core 2.1 web application that contains a SignalR Hub.
I need to make a .net core 2.1 console application that send information to my hub on my web system.
I did a development here, however when I run my console app, no exception appears, but my web application does not receive the information.
See what I've done so far.
Aspnet Core 2.1
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddDistributedMemoryCache();
services.AddSession();
services.Configure<CookiePolicyOptions>(options =>
{
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddSignalR();
services.AddMvc(
config => {
var policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
config.Filters.Add(new AuthorizeFilter(policy));
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddAuthentication(options =>
{
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
}).AddCookie(CookieAuthenticationDefaults.AuthenticationScheme,
options =>
{
options.ExpireTimeSpan = TimeSpan.FromDays(1);
options.LoginPath = "/Home/Login";
options.LogoutPath = "/Usuario/Logout";
options.SlidingExpiration = true;
});
//provedor postgresql
services.AddEntityFrameworkNpgsql()
.AddDbContext<MesContext>(options =>
options.UseNpgsql(Configuration.GetConnectionString("MesContext")));
services.AddScoped<SeedingService>();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, SeedingService seedingService)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
seedingService.Seed();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAuthentication();
app.UseSession();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
app.UseSignalR(routes =>
{
routes.MapHub<MesHub>("/MesHub");
});
}
}
View Index
//teste
// ATUALIZA LISTAGEM
connection.on("teste", function (sucesso, msg) {
if (sucesso) {
console.log(msg);
}
});
Hub
public class MesHub : Hub
{
public static ConcurrentDictionary<string, List<string>> ConnectedUsers = new ConcurrentDictionary<string, List<string>>();
public void SendAsync(string message)
{
// Call the addMessage method on all clients
Clients.All.SendAsync("teste", true, message);
}
public override Task OnConnectedAsync()
{
string name = Context.User.Identity.Name;
Groups.AddToGroupAsync(Context.ConnectionId, name);
return base.OnConnectedAsync();
}
}
Console aplication
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Teste Renan!");
var url = "https://localhost:44323/meshub";
var connection = new HubConnectionBuilder()
.WithUrl($"{url}")
.WithAutomaticReconnect() //I don't think this is totally required, but can't hurt either
.Build();
var t = connection.StartAsync();
Console.WriteLine("conected!");
//Wait for the connection to complete
t.Wait();
Console.WriteLine(connection.State);
//connection.SendAsync("SendAsync", "renan");
connection.InvokeAsync<string>("SendAsync", "HELLO World ").ContinueWith(task => {
if (task.IsFaulted)
{
Console.WriteLine("There was an error calling send: {0}",
task.Exception.GetBaseException());
}
else
{
Console.WriteLine(task.Result);
}
});
Console.WriteLine("enviado!");
}
}
In the console app I add the Microsoft.aspnetCore.SignalR.Client package
Signalr works very well within the aspnet core web system, as it is not receiving information externally.
I'm forgetting something? what am I doing wrong?
I define same hub method and do a test to invoke it from the console client, which work as expected.
Besides, I test it with my SignalR console client app, it works well too. If possible, you can test it and check if it works for you.
static void Main(string[] args)
{
Console.WriteLine("Client App Starts...");
Program p = new Program();
p.Myfunc().Wait();
Console.ReadLine();
}
public async Task Myfunc()
{
HubConnection connection = new HubConnectionBuilder()
.WithUrl("https://localhost:44323/meshub")
.Build();
connection.Closed += async (error) =>
{
await Task.Delay(new Random().Next(0, 5) * 1000);
await connection.StartAsync();
};
await connection.StartAsync();
try
{
await connection.InvokeAsync("SendAsync", "HELLO World ");
//await connection.InvokeAsync("SendMessage", "console app", "test mes");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
Test Result
Updated:
in my Hub I have an OnConnectedAsync method where the name of the group is informed
You can try to update the OnConnectedAsync method as below, then check if your SignalR console client app can connect to hub and communicate with web client well.
public override async Task OnConnectedAsync()
{
string name = Context.User.Identity.Name;
if (name != null)
{
await Groups.AddToGroupAsync(Context.ConnectionId, name);
}
await base.OnConnectedAsync();
}

Can a SignalR Hub receive events from clients? And if so, how?

I have a signalR hub that needs to be able to receive an event from a client and then notify all other clients connected to the hub.
Is that possible?
I want my 'hub' application to be able to receive messages and send them. I can only figure out how to do the sending of messages. Here is what I have now:
Application 1-- Hub
Startup class:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddSignalR().AddHubOptions<EventsHub>(options =>
{
options.HandshakeTimeout = TimeSpan.FromMinutes(5);
options.EnableDetailedErrors = true;
});
services.AddTransient(typeof(BusinessLogic.EventsBusinessLogic));
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
app.UseSignalR((configure) =>
{
configure.MapHub<EventsHub>("/hubs/events", (options) =>
{
});
});
}
Set Up of the Hub in Application 1
public class EventsHub : Hub
{
public EventsHub()
{
}
public override Task OnConnectedAsync()
{
if (UserHandler.ConnectedIds.Count == 0)
{
//Do something on connect
}
UserHandler.ConnectedIds.Add(Context.ConnectionId);
Console.WriteLine("Connection:");
return base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception exception)
{
//Do something on Disconnect
}
public static class UserHandler
{
public static HashSet<string> ConnectedIds = new HashSet<string>();
}
}
BusinessLogic:
public class EventsBusinessLogic
{
private readonly IHubContext<EventsHub> _eventsHub;
public EventsBusinessLogic(IHubContext<EventsHub> eventsHub)
{
_eventsHub = eventsHub;
}
public async Task<Task> EventReceivedNotification(ProjectMoonEventLog eventInformation)
{
try
{
await _eventsHub.Clients.All.SendAsync("NewEvent", SomeObject);
}
catch (Exception e)
{
throw new Exception(e.Message);
}
}
}
In the second application, that listens for events or messages from the hub:
Startup.cs
private static void ConfigureAppServices(IServiceCollection services, string Orale, string Sql)
{
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddOptions();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
//set up of singletons and transients
services.AddHostedService<Events.EventingHubClient>();
}
The ClientHub to connect to application 1:
public class EventingHubClient : IHostedService
{
private HubConnection _connection;
public EventingHubClient()
{
_connection = new HubConnectionBuilder()
.WithUrl("http://localhost:61520/hubs/events")
.Build();
_connection.On<Event>("NewEvent",
data => _ = EventReceivedNotification(data));
}
public async Task<Task> EventReceivedNotification(Event eventInformation)
{
try
{
//Do something when the event happens
return Task.CompletedTask;
}
catch (Exception e)
{
throw new Exception(e.Message);
}
}
public async Task StartAsync(CancellationToken cancellationToken)
{
// Loop is here to wait until the server is running
while (true)
{
try
{
await _connection.StartAsync(cancellationToken);
Console.WriteLine("Connected");
break;
}
catch (Exception e)
{
Console.WriteLine(e.Message);
await Task.Delay(100);
}
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
return _connection.DisposeAsync();
}
}
This works, but now I want application 2 to be able to send a message to application 1? So I need a similar piece of code as in the EventsBusinessLogic class in application2 to send messages to application 1.
I hope this is clear enough? Is this the purpose of SignalR?
Please refer to signalR documentation signalR documentation for .net client
I guess in your Hub method like this
public async Task SendTransaction(Transaction data)
{
await Clients.All.SendAsync("TransactionReceived", data);
}
Then add methods in client side
in constructor add
connection.On<Transaction>("TransactionReceived", (data) =>
{
this.Dispatcher.Invoke(() =>
{
var transactionData = data;
});
});
and then SendTransaction expected on server
private async void SendTransaction(Transaction data)
{
try
{
await connection.InvokeAsync("SendTransaction", data);
}
catch (Exception ex)
{
//
throw
}
}

CQRS ValidatorHandler not recognizing FluentValidation validators?

I'm using Web Api 2, Autofac, and MediatR (CQRS). I have a mediator pipeline in place that has pre/post request handlers. That all works fine. I'm trying to hook up Validation now and decorate the pipeline with it.
Here is my Autofac DI code:
public void Configuration(IAppBuilder app)
{
var config = new HttpConfiguration();
FluentValidationModelValidatorProvider.Configure(config);
ConfigureDependencyInjection(app, config);
WebApiConfig.Register(config);
app.UseWebApi(config);
}
private static void ConfigureDependencyInjection(IAppBuilder app, HttpConfiguration config)
{
var builder = new ContainerBuilder();
builder.RegisterSource(new ContravariantRegistrationSource());
builder.RegisterAssemblyTypes(typeof(IMediator).Assembly).AsImplementedInterfaces();
builder.Register<SingleInstanceFactory>(ctx =>
{
var c = ctx.Resolve<IComponentContext>();
return t => c.Resolve(t);
});
builder.Register<MultiInstanceFactory>(ctx =>
{
var c = ctx.Resolve<IComponentContext>();
return t => (IEnumerable<object>)c.Resolve(typeof(IEnumerable<>).MakeGenericType(t));
});
//register all pre handlers
builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly())
.As(type => type.GetInterfaces()
.Where(interfacetype => interfacetype.IsClosedTypeOf(typeof(IAsyncPreRequestHandler<>))))
.InstancePerLifetimeScope();
//register all post handlers
builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly())
.As(type => type.GetInterfaces()
.Where(interfacetype => interfacetype.IsClosedTypeOf(typeof(IAsyncPostRequestHandler<,>))))
.InstancePerLifetimeScope();
//register all async handlers
builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly())
.As(type => type.GetInterfaces()
.Where(interfaceType => interfaceType.IsClosedTypeOf(typeof(IAsyncRequestHandler<,>)))
.Select(interfaceType => new KeyedService("asyncRequestHandler", interfaceType)))
.InstancePerLifetimeScope();
//register pipeline decorator
builder.RegisterGenericDecorator(
typeof(AsyncMediatorPipeline<,>),
typeof(IAsyncRequestHandler<,>),
"asyncRequestHandler")
.Keyed("asyncMediatorPipeline", typeof(IAsyncRequestHandler<,>))
.InstancePerLifetimeScope();
//register validator decorator
builder.RegisterGenericDecorator(
typeof(ValidatorHandler<,>),
typeof(IAsyncRequestHandler<,>),
"asyncMediatorPipeline")
.InstancePerLifetimeScope();
// Register Web API controller in executing assembly.
builder.RegisterApiControllers(Assembly.GetExecutingAssembly()).InstancePerRequest();
//register RedStripeDbContext
builder.RegisterType<RedStripeDbContext>().As<IRedStripeDbContext>().InstancePerRequest();
builder.RegisterType<AutofacServiceLocator>().AsImplementedInterfaces();
var container = builder.Build();
config.DependencyResolver = new AutofacWebApiDependencyResolver(container);
// This should be the first middleware added to the IAppBuilder.
app.UseAutofacMiddleware(container);
// Make sure the Autofac lifetime scope is passed to Web API.
app.UseAutofacWebApi(config);
}
Here is the ValidatorHandler:
public class ValidatorHandler<TRequest, TResponse> : IAsyncRequestHandler<TRequest, TResponse> where TRequest : IAsyncRequest<TResponse>
{
private readonly IAsyncRequestHandler<TRequest, TResponse> _inner;
private readonly IValidator<TRequest>[] _validators;
public ValidatorHandler(
IAsyncRequestHandler<TRequest, TResponse> inner,
IValidator<TRequest>[] validators)
{
_inner = inner;
_validators = validators;
}
public async Task<TResponse> Handle(TRequest request)
{
var context = new ValidationContext(request);
var failures = _validators
.Select(v => v.Validate(context))
.SelectMany(result => result.Errors)
.Where(f => f != null)
.ToList();
if (failures.Any())
throw new ValidationException(failures);
return await _inner.Handle(request);
}
}
Here is a sample query:
[Validator(typeof(GetAccountRequestValidationHandler))]
public class GetAccountRequest : IAsyncRequest<GetAccountResponse>
{
public int Id { get; set; }
}
Here is the fluent validation handler:
public class GetAccountRequestValidationHandler : AbstractValidator<GetAccountRequest>
{
public GetAccountRequestValidationHandler()
{
RuleFor(m => m.Id).GreaterThan(0).WithMessage("Please specify an id.");
}
public Task Handle(GetAccountRequest request)
{
Debug.WriteLine("GetAccountPreProcessor Handler");
return Task.FromResult(true);
}
}
Here is the request handler:
public class GetAccountRequestHandler : IAsyncRequestHandler<GetAccountRequest, GetAccountResponse>
{
private readonly IRedStripeDbContext _dbContext;
public GetAccountRequestHandler(IRedStripeDbContext redStripeDbContext)
{
_dbContext = redStripeDbContext;
}
public async Task<GetAccountResponse> Handle(GetAccountRequest message)
{
return await _dbContext.Accounts.Where(a => a.AccountId == message.Id)
.ProjectToSingleOrDefaultAsync<GetAccountResponse>();
}
}
Finally here is the Web Api 2 HttpGet method:
[Route("{id:int}")]
[HttpGet]
public async Task<IHttpActionResult> GetById([FromUri] GetAccountRequest request)
{
var model = await _mediator.SendAsync<GetAccountResponse>(request);
return Ok(model);
}
I put breakpoints all over the place and when I hit this endpoint, the first thing I get into is the GetAccountRequestValidationHandler. Then I get into the ValidatorHandler's constructor. The problem is, the IValidator[] validators parameter to the constructor is always null.
I must be missing something with fluent validation and its registration via Autofac? Any help is much appreciated.
The validator types must be registered in the IoC. Adding the below to your ConfigureDependencyInjection method should do it.
builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly())
.Where(t => t.Name.EndsWith("ValidationHandler"))
.AsImplementedInterfaces()
.InstancePerLifetimeScope();