Cast exception when validation failed in Fluent Validation - c#

I have a .net 6 WebApi which I am using Fluent Validation with MediatR. I have everything working when there is no validation errors.
When I force an error I get the following Exception.
Unable to cast object of type 'System.Collections.Generic.Dictionary`2[System.String,System.String[]]' to type 'System.Collections.Generic.IEnumerable`1[FluentValidation.Results.ValidationFailure]'.
at TestMediatR.Behaviours.ValidationBehaviour`2.Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate`1 next)
at MediatR.Pipeline.RequestExceptionProcessorBehavior`2.Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate`1 next)
at MediatR.Pipeline.RequestExceptionProcessorBehavior`2.Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate`1 next)
at MediatR.Pipeline.RequestExceptionActionProcessorBehavior`2.Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate`1 next)
at MediatR.Pipeline.RequestExceptionActionProcessorBehavior`2.Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate`1 next)
at MediatR.Pipeline.RequestPostProcessorBehavior`2.Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate`1 next)
at MediatR.Pipeline.RequestPreProcessorBehavior`2.Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate`1 next)
at WebApi.Controllers.v1.OrdersController.AddOrder(OrderTicketDto model) in D:\Git Repositories\Current\Web-Sites\RestWebApi\src\WebApi\Controllers\v1\OrdersController.cs:line 36
Code executing the mediatR send is this.
[HttpPost("AddOrder")]
public async Task<IActionResult> AddOrder([FromBody] OrderTicketDto model)
{
_logger.LogInformation("Adding Order: {#model}", model);
try
{
var response = await Mediator.Send(new AddOrderCommand()
{
OrderData = model.OrderTicket,
Url = model.SiteUrl,
Token = model.Token
});
return Ok(response);
}
catch (Exception ex)
{
_logger.LogError(ex, "Add Order Error"); //<------ FluentValidation exception caught here
return BadRequest(ex.Message);
}
}
and validation for the command executed above is done like this
public class AddOrderCommandValidator : AbstractValidator<AddOrderCommand>
{
public AddOrderCommandValidator()
{
RuleFor(x => x.Url)
.NotEmpty()
.NotNull();
RuleFor(x => x.Token)
.NotEmpty()
.NotNull();
RuleFor(x => x.OrderData)
.NotNull();
}
}
Register of the validators is done here in startup
public static IServiceCollection AddPiKSRestValidators(this IServiceCollection services)
{
var domainAssembly = typeof(GetTablesCommandValidator).GetTypeInfo().Assembly;
//Add FluentValidation
services.AddValidatorsFromAssembly(domainAssembly);
return services;
}
As I say, everything works when I pass valid properties, but force it to be invalid by setting say Token property to null and I get the exception.
Feel like I am missing something.

So the issue was with the ValidationBehaviour
here is the code to report the errors
public class ValidationBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : notnull, IRequest<TResponse>
{
private readonly IEnumerable<IValidator<TRequest>> _validators;
private readonly ILogger<TRequest> _logger;
public ValidationBehaviour(IEnumerable<IValidator<TRequest>> validators, ILogger<TRequest> logger)
{
_validators = validators;
_logger = logger;
}
public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
{
if (_validators.Any())
{
var context = new ValidationContext<TRequest>(request);
var errorsDictionary = _validators
.Select(x => x.Validate(context))
.SelectMany(x => x.Errors)
.Where(x => x != null)
.GroupBy(
x => x.PropertyName,
x => x.ErrorMessage,
(propertyName, errorMessages) => new
{
Key = propertyName,
Values = errorMessages.Distinct().ToArray()
})
.ToDictionary(x => x.Key, x => x.Values);
if (errorsDictionary.Any())
{
throw new ValidationException((IEnumerable<FluentValidation.Results.ValidationFailure>)errorsDictionary);
}
}
else
_logger.LogDebug("No Validators found");
return await next();
}
}
As you can see a dictionary is trying to be cast to (IEnumerable<FluentValidation.Results.ValidationFailure>)
fixed with this.
var errorsDictionary = _validators
.Select(x => x.Validate(context))
.SelectMany(x => x.Errors)
.Where(x => x != null)
.GroupBy(x => new {x.PropertyName, x.ErrorMessage })
.Select(x => x.FirstOrDefault())
.ToList();
if (errorsDictionary.Any())
{
throw new ValidationException(errorsDictionary);
}

Related

Exception thrown in middleware being logged as error instead of warning

I am having quite a few exceptions being logged for clients disconnecting, but they are being logged as errors. I want to "downgrade" them to warnings, because when I monitor my logs, these logs really aren't something I want to see. How do I downgrade them to warning?
Connection ID "17726168134941057923", Request ID "8002a384-0000-f600-b63f-84710c7967bb": An unhandled exception was thrown by the application. Microsoft.AspNetCore.Connections.ConnectionResetException: The client has disconnected ---> System.Runtime.InteropServices.COMException (0x80070040): The specified network name is no longer available. (0x80070040)
--- End of inner exception stack trace ---
at Microsoft.AspNetCore.Server.IIS.Core.IO.AsyncIOOperation.GetResult(Int16 token)
at Microsoft.AspNetCore.Server.IIS.Core.IISHttpContext.ReadBody()
at System.IO.Pipelines.Pipe.GetReadResult(ReadResult& result)
at System.IO.Pipelines.Pipe.GetReadAsyncResult()
at Microsoft.AspNetCore.Server.IIS.Core.IISHttpContext.ReadAsync(Memory`1 memory, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Server.IIS.Core.HttpRequestStream.ReadAsyncInternal(Memory`1 buffer, CancellationToken cancellationToken)
at Microsoft.AspNetCore.WebUtilities.FileBufferingReadStream.ReadAsync(Memory`1 buffer, CancellationToken cancellationToken)
at System.IO.StreamReader.ReadBufferAsync(CancellationToken cancellationToken)
at System.IO.StreamReader.ReadToEndAsyncInternal()
at MyAPI.Middleware.LoggingMiddleware.InvokeAsync(HttpContext context)
at MyAPI.Middleware.CorsMessageMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Builder.Extensions.UsePathBaseMiddleware.InvokeCore(HttpContext context, String matchedPath, String remainingPath)
at Microsoft.AspNetCore.Server.IIS.Core.IISHttpContextOfT`1.ProcessRequestAsync()
My Startup.Configure looks like this:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IOptions<AppSettings> options)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseSwagger();
app.UseSwaggerUI(config =>
{
config.SwaggerEndpoint("v1/swagger.json", "My API");
config.SupportedSubmitMethods(Array.Empty<SubmitMethod>());
});
app.UseMiddleware<CorsMessageMiddleware>();
app.UseMiddleware<LoggingMiddleware>();
app.UseMiddleware<ExceptionMiddleware>();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
app.UseHealthChecks("/health");
}
I have the exceptions caught in ExceptionMiddleware, but it seems the client disconnects somewhere above in the pipeline.
public class ExceptionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ExceptionMiddleware> _logger;
public ExceptionMiddleware(RequestDelegate next, ILogger<ExceptionMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (TaskCanceledException ex)
{
_logger.LogWarning(ex, ex.Message);
await HandleExceptionAsync(context);
}
catch (OperationCanceledException ex)
{
_logger.LogWarning(ex, ex.Message);
await HandleExceptionAsync(context);
}
catch (COMException ex)
{
_logger.LogWarning(ex, ex.Message);
await HandleExceptionAsync(context);
}
catch (ConnectionResetException ex)
{
_logger.LogWarning(ex, ex.Message);
await HandleExceptionAsync(context);
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
await HandleExceptionAsync(context);
}
}
private static Task HandleExceptionAsync(HttpContext context)
{
var result = new ActionResult<object>
{
Messages = new List<string> { "INTERNAL_SERVER_ERROR" }
};
context.Response.StatusCode = 500;
context.Response.ContentType = "application/json";
return context.Response.WriteAsync(SerializeObject(result));
}
private static string SerializeObject(object data)
{
return System.Text.Json.JsonSerializer.Serialize(data);
}
}
LoggingMiddleware:
public class LoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<LoggingMiddleware> _logger;
private readonly List<string> _contentSubTypes = new List<string>
{
"text",
"json",
"xml",
"*/*",
};
public LoggingMiddleware(ILogger<LoggingMiddleware> logger, RequestDelegate next)
{
_logger = logger;
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
// WebSocket requests should be avoided
if (context.WebSockets.IsWebSocketRequest)
{
return;
}
StringBuilder log = new StringBuilder();
// Request
log.AppendLine($"A {context.Request.Method} request arrived at {context.Request.Path}{context.Request.QueryString}");
log.AppendLine($"Request time (local): {DateTime.Now:r}");
log.AppendLine($"Request headers: {{{string.Join("; ", context.Request.Headers.Select(x => $"{x.Key}:{x.Value}"))}}}");
var supportedMethods = new[] { "POST", "PATCH", "PUT" };
if (supportedMethods.Any(x => x.Equals(context.Request.Method, StringComparison.OrdinalIgnoreCase)))
{
context.Request.EnableBuffering();
using (var reader = new StreamReader(context.Request.Body, Encoding.UTF8, true, 10240, true))
{
string body = await reader.ReadToEndAsync();
string bodyLog = !string.IsNullOrWhiteSpace(body) ? body : "EMPTY";
log.AppendLine($"Body: {bodyLog}");
context.Request.Body.Seek(0, SeekOrigin.Begin);
}
}
// Response
if (!_contentSubTypes.Any(subType => context.Request.Headers["Accept"].Any(acceptHeaderValue => acceptHeaderValue.Contains(subType))))
{
await _next(context);
var response = context.Response;
log.AppendLine($"Response time (local): {DateTime.Now:r}");
log.AppendLine($"Response status: {response.StatusCode}");
log.AppendLine($"Response headers: {string.Join("; ", response.Headers.Select(x => $"{x.Key}:{x.Value}"))}");
}
else
{
var originalBodyStream = context.Response.Body;
using (var responseBody = new MemoryStream())
{
context.Response.Body = responseBody;
// Invoke next middleware
await _next(context);
var response = context.Response;
log.AppendLine($"Response time (local): {DateTime.Now:r}");
log.AppendLine($"Response status: {response.StatusCode}");
log.AppendLine($"Response headers: {string.Join("; ", response.Headers.Select(x => $"{x.Key}:{x.Value}"))}");
response.Body.Seek(0, SeekOrigin.Begin);
// response.ContentType is NULL empty when querying stuff such as favicon.ico
if (!string.IsNullOrEmpty(response.ContentType)
&& _contentSubTypes.Any(subType => response.ContentType.Contains(subType)))
{
using (var streamReader = new StreamReader(response.Body, Encoding.UTF8, true, 10240, true))
{
string responseText = await streamReader.ReadToEndAsync();
string bodyLog = !string.IsNullOrWhiteSpace(responseText) ? responseText : "EMPTY";
response.Body.Seek(0, SeekOrigin.Begin);
log.AppendLine($"Body: {bodyLog}");
}
}
await responseBody.CopyToAsync(originalBodyStream);
}
}
await Task.Run(() =>
{
_logger.LogDebug(log.ToString());
return Task.CompletedTask;
});
}
}

Using an ILogger in a Polly Policy attached to a Refit Client

I've been trying to follow the directions from this blog post to pass an ILogger to my retry policy in order to log information about the errors being retried.
The code in the blog doesn't work out of the box as we're using Refit for client generation. Based on the refit docs it should just be a matter of adding a property to my method signatures, but haven't been able to get it to actually work.
Even though I've added the property to my method signature:
Task<UserSubscriptions> GetUserSubscriptions(string userId, [Property("PollyExecutionContext")] Polly.Context context);
I've captured logger management in extension methods:
private static readonly string LoggerKey = "LoggerKey";
public static Context WithLogger(this Context context, ILogger logger)
{
context[LoggerKey] = logger;
return context;
}
public static ILogger GetLogger(this Context context)
{
if (context.TryGetValue(LoggerKey, out object logger))
{
return logger as ILogger;
}
return null;
}
I create a new context when executing the method:
public Context GetPollyContext() => new Context().WithLogger(logger);
public Task<UserSubscriptions> GetUserSubscriptions(UserId userId) {
return restClient.GetUserSubscriptions(userId.UserIdString, GetPollyContext());
}
And try to access the logger as part of the retry action:
return Policy
.Handle<Exception>()
.OrResult<HttpResponseMessage>(r => CodesToRetry.Contains(r.StatusCode))
.WaitAndRetryAsync(3, retryCount => TimeSpan.FromSeconds(1), (result, timeSpan, retryCount, context) =>
{
var logger = context.GetLogger();
if (logger == null) return;
// do some logging
}
});
When I set a break point in the retry action the context that I see is a new empty context and not the one I created with the attached logger.
Per GitHub issues, there was a typo, the property is PolicyExecutionContext, not PollyExecutionContext.
Though given I don't need to generate a unique context per request, the better pattern is to use delegate injection.
Extension methods
private static readonly string LoggerKey = "LoggerKey";
public static Context WithLogger(this Context context, ILogger logger)
{
context[LoggerKey] = logger;
return context;
}
public static ILogger GetLogger(this Context context)
{
if (context.TryGetValue(LoggerKey, out object logger))
{
return logger as ILogger;
}
return null;
}
Delegate definition
public class PollyContextInjectingDelegatingHandler<T> : DelegatingHandler
{
private readonly ILogger<T> _logger;
public PollyContextInjectingDelegatingHandler(ILogger<T> logger)
{
_logger = logger;
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
{
var pollyContext = new Context().WithLogger(_logger);
request.SetPolicyExecutionContext(pollyContext);
return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
}
}
Then add the delegate to the client definition
services
.AddTransient<ISubscriptionApi, SubscriptionApi>()
.AddTransient<PollyContextInjectingDelegatingHandler<SubscriptionApi>>()
.AddRefitClient<ISubscriptionApiRest>(EightClientFactory.GetRefitSettings())
.ConfigureHttpClient((s, c) =>
{
...
})
.AddHttpMessageHandler<PollyContextInjectingDelegatingHandler<SubscriptionApi>>()
.ApplyTransientRetryPolicy(retryCount, timeout);

How do you know if an url is pointing to an endpoint in ASP.net core?

I create a middleware
public class CustomMiddleware
{
private readonly RequestDelegate _next;
public CustomMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
if (IsPage(context))
{
// Do something
}
await _next(context);
}
}
I want IsPage(context) to response like this
get test.com -> true
get test.com/about -> true
get test.com/product/1 -> true
get test.com/main.js -> false
get test.com/main.css -> false
post test.com/search -> false
How do I do this?
If you place your custom middleware after UseRouting in the middleware pipeline, you can use GetEndpoint, which returns null if the request URL was not mapped to an endpoint:
app.Use(async (context, next) =>
{
if (HttpMethods.IsGet(context.Request.Method)
&& context.GetEndpoint() != null)
{
// code
}
await next();
});

Rebus.Async with SimpleInjector Request/Reply MessageCouldNotBeDispatchedToAnyHandlersException

I'm using Rebus.Async to reply a processing data to WebApi request using command CQRS.
1. I registered bus with the following configuration:
public static IContainerRegistry RegisterRebusInMemory(
this IContainerRegistry containerRegistry)
{
switch (_containerAdapter)
{
default: // SimpleInjector
return RegisterRebusInMemoryWithSimpleInjectorContainer(containerRegistry);
}
}
private static IContainerRegistry RegisterRebusInMemoryWithSimpleInjectorContainer(
IContainerRegistry containerRegistry)
{
var rebusQueue = "rebusMessages";
Container container = (Container)containerRegistry.Container;
RegisterRebus(containerRegistry);
container.ConfigureRebus(
configurer => configurer
.Logging(l => l.Console())
.Serialization(s => s.UseNewtonsoftJson(NewtonsoftSettings, Encoding.UTF8))
.Options(o =>
{
o.EnableSynchronousRequestReply();
o.SetNumberOfWorkers(1);
o.SetMaxParallelism(1);
})
.Transport(t => t.UseInMemoryTransport(new InMemNetwork(), rebusQueue))
.Routing(r =>
{
r.TypeBased()
.MapAssemblyOf<IDomainNotification>(rebusQueue)
.MapAssemblyOf<DefaultReplyCommand>(rebusQueue)
.MapCommands(rebusQueue)
.MapEvents(rebusQueue);
})
.Sagas(sagas => sagas.StoreInMemory())
.Subscriptions(s => s.StoreInMemory())
.Start()
);
return containerRegistry;
}
2. I create a Handle to use with request/reply:
public class MyCommandHandler : CommandHandler, IHandleMessages<MyCommand>
{
...
public async Task Handle(MyCommand message)
{
DefaultReplyCommand defaultReply = new DefaultReplyCommand(message.AggregateId);
// Do some stuffs
await _bus.Reply(_reply);
}
...
}
3. When I send the reply with await _bus.Reply(_reply) throw the following exception:
[WRN] Rebus.Retry.ErrorTracking.InMemErrorTracker (Rebus 1 worker 1): Unhandled exception 1 (FINAL) while handling message with ID "request-reply_3452be37-fdfd-4115-89a2-504b4feae22f" Rebus.Exceptions.MessageCouldNotBeDispatchedToAnyHandlersException: Message with ID request-reply_3452be37-fdfd-4115-89a2-504b4feae22f and type AgroHUB.Application.Cattle.Commands.CattleOperation.MoveCattleOperationCommand, AgroHUB.Application.Cattle could not be dispatched to any handlers (and will not be retried under the default fail-fast settings)
at Rebus.Pipeline.Receive.DispatchIncomingMessageStep.Process(IncomingStepContext context, Func`1 next)
at Rebus.Sagas.LoadSagaDataStep.Process(IncomingStepContext context, Func`1 next)
at Rebus.Pipeline.Receive.ActivateHandlersStep.Process(IncomingStepContext context, Func`1 next)
at Rebus.Internals.ReplyHandlerStep.Process(IncomingStepContext context, Func`1 next)
at Rebus.Pipeline.Receive.HandleRoutingSlipsStep.Process(IncomingStepContext context, Func`1 next)
at Rebus.Pipeline.Receive.DeserializeIncomingMessageStep.Process(IncomingStepContext context, Func`1 next)
at Rebus.DataBus.ClaimCheck.HydrateIncomingMessageStep.Process(IncomingStepContext context, Func`1 next)
at Rebus.Pipeline.Receive.HandleDeferredMessagesStep.Process(IncomingStepContext context, Func`1 next)
at Rebus.Retry.FailFast.FailFastStep.Process(IncomingStepContext context, Func`1 next)
at Rebus.Retry.Simple.SimpleRetryStrategyStep.DispatchWithTrackerIdentifier(Func`1 next, String identifierToTrackMessageBy, ITransactionContext transactionContext, String messageId, String secondLevelMessageId)
Cenario
The Rebus.Async works with native C# Dependency Injection (service.AddScope), but when I change de Cointainer to use Simple Injector the reply doesn't work anymore.
There's some extra configuration to use Rebus.Async with Simple Injector?

Use custom HttpMessageHandler with different configurations

I have a custom HttpMessageHandler implementation:
public class MyHandler : DelegatingHandler
{
private readonly HttpClient _httpClient;
private readonly MyHandlerOptions _config;
public MyHandler(
HttpClient httpClient,
IOptions<MyHandlerOptions> options)
{
_httpClient = httpClient;
_config = options.Value;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
request.Headers.Authorization = await GetAccessToken()
return await base.SendAsync(request, cancellationToken);
}
private async Task<string> GetAccessToken()
{
//some logic to get access token using _httpClient and _config
}
}
It requires confiuration object MyHandlerOptions. Its form is not so important here. It basically contains clientId, clientSecret, etc. that are needed for the handler to know how to get the access token.
I have a few services (typed http clients) that need to use MyHandler:
//registration of MyHandler itself
builder.Services.AddHttpClient<MyHandler>();
//configuration of MyHandler
builder.Services.AddOptions<MyHandlerOptions>()
.Configure<IConfiguration>((config, configuration) =>
{
configuration.GetSection("MyHandlerOptions").Bind(config);
});
//Services that need to use MyHandler:
services.AddHttpClient<Service1>()
.AddHttpMessageHandler<MyHandler>();
services.AddHttpClient<Service2>()
.AddHttpMessageHandler<MyHandler>();
services.AddHttpClient<Service3>()
.AddHttpMessageHandler<MyHandler>();
The problem is that the MyHandlerOptions instance that I registered is valid only when used with Service1. However, Service2 and Service3 require other configuration (different clientId, clientSecret, etc.). How can I achieve it?
The possible solution that comes to my mind:
Create a new service:
public class AccessTokenGetter
{
Task<string> GetAccessToken(AccessTokenConfig config)
{
//get the access token...
}
}
Create separate HttpMessageHandlers for each case where configuration is different:
public class MyHandler1 : DelegatingHandler
{
private readonly MyHandler1Options _config;
private readonly AccessTokenGetter _accessTokenGetter;
public MyHandler(AccessTokenGetter accessTokenGetter, IOptions<MyHandlerOptions1> options)
{
_accessTokenGetter = accessTokenGetter;
_config = options.Value;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
//somehow convert _config to AccessTokenConfig
request.Headers.Authorization = await _accessTokenGetter.GetAccessToken(_config)
return await base.SendAsync(request, cancellationToken);
}
}
public class MyHandler2 : DelegatingHandler
{
//same implementation as MyHandler1, just use MyHandler2Options instead
}
Register my services:
//configurations
builder.Services.AddOptions<MyHandler1Options>()
.Configure<IConfiguration>((config, configuration) =>
{
configuration.GetSection("MyHandler1Options").Bind(config);
});
builder.Services.AddOptions<MyHandler2Options>()
.Configure<IConfiguration>((config, configuration) =>
{
configuration.GetSection("MyHandler2Options").Bind(config);
});
//AccessTokenGetter
services.AddHttpClient<AccessTokenGetter>()
//Services that need to use MyHandlers:
services.AddHttpClient<Service1>()
.AddHttpMessageHandler<MyHandler1>();
services.AddHttpClient<Service2>()
.AddHttpMessageHandler<MyHandler2>();
services.AddHttpClient<Service3>()
.AddHttpMessageHandler<MyHandler2>();
Is there a better solution? I am not a great fan of my idea, it is not very flexible.
services.AddHttpClient<Service1>()
.AddHttpMessageHandler(sp =>
{
var handler = sp.GetRequiredService<MyHandler>();
handler.Foo = "Bar";
return handler;
});
services.AddHttpClient<Service2>()
.AddHttpMessageHandler(sp =>
{
var handler = sp.GetRequiredService<MyHandler>();
handler.Foo = "Baz";
return handler;
});

Categories