Best approach for a middleware in an application running jobs - c#

I have a service that calls endpoints from a external API. I developed a custom middleware for handling possible exceptions from these API calls and logging them. However, although the middleware includes a try and catch block in the Invoke method, my application's flow is being interrupted after an Exception is caught by the catch block. The flow simply ends, although the application keeps running. As this application is executing jobs that sync data, I need that the flow continues (and possible exceptions be logged, as I mentioned).
Anyone has already experienced this or have any clue how to make the flow go on? Is there a better approach for doing this? I'm totally open for suggestions, please.
This is my code:
public class ApiException
{
private readonly RequestDelegate _next;
private readonly ILogger<ApiException> _logger;
private readonly ApiExceptionOptions _options;
public ApiException(ApiExceptionOptions options, RequestDelegate next,
ILogger<ApiException> logger)
{
_next = next;
_logger = logger;
_options = options;
}
public async Task Invoke(HttpContext context /* other dependencies */)
{
try
{
await _next(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex);
}
}
private Task HandleExceptionAsync(HttpContext context, Exception exception)
{
ApiError error = _options.AddResponseDetails?.Invoke(exception)
?? ApiErrorFactory.New(exception);
LogApiException(exception, error);
return CreateResponse(context, error);
}
private static Task CreateResponse(HttpContext context, ApiError error)
{
var result = JsonSerializer.Serialize(error,
new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
});
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)error.Status;
return context.Response.WriteAsync(result);
}
private void LogApiException(Exception exception, ApiError error)
{
var innerExMessage = GetInnermostExceptionMessage(exception);
var level = _options.DetermineLogLevel?.Invoke(exception) ?? GetLogLevel(exception);
_logger.Log(level, exception, "ERROR: {message} -- {ErrorId}.", innerExMessage, error.Id);
}
private static LogLevel GetLogLevel(Exception exception) =>
exception switch
{
InvalidInputException _ => LogLevel.Warning,
_ => LogLevel.Error
};
private string GetInnermostExceptionMessage(Exception exception)
{
if (exception.InnerException != null)
return GetInnermostExceptionMessage(exception.InnerException);
return exception.Message;
}
}
public class ApiExceptionOptions
{
public Func<Exception, ApiError> AddResponseDetails { get; set; }
public Func<Exception, LogLevel> DetermineLogLevel { get; set; }
}
public static class ApiErrorFactory
{
public static ApiError New(Exception exception) =>
exception switch
{
InvalidInputException e => new ApiError
{
Details = e.Details,
Message = e.Message,
Status = BadRequest
},
_ => new ApiError()
};
}
public class ApiError
{
public static string DefaultErrorMessage = "Erro no processamento da requisição.";
public string Id { get; } = Guid.NewGuid().ToString();
public HttpStatusCode Status { get; set; } = HttpStatusCode.InternalServerError;
public string Title { get; set; } = "API Error";
public string Message { get; set; } = DefaultErrorMessage;
public IDictionary<string, object> Details { get; set; } = new Dictionary<string, object>();
}
public static class ApiExceptionExtensions
{
public static IApplicationBuilder UseApiExceptionHandler(this IApplicationBuilder builder)
{
var options = new ApiExceptionOptions();
return builder.UseMiddleware<ApiException>(options);
}
public static IApplicationBuilder UseApiExceptionHandler(this IApplicationBuilder builder,
Action<ApiExceptionOptions> configureOptions)
{
var options = new ApiExceptionOptions();
configureOptions(options);
return builder.UseMiddleware<ApiException>(options);
}
}

Related

Route not found exception with the generic response model

I am trying to capture the exception when the route is not found and wrap the exception with the generic response model.
I tried to implement, as given in the answer to the question, but this solution also doesn't seem to work in my use case.
Because the status code 404 is also added to the response when the resource is not found, like when Id is not found.
app.UseStatusCodePages(new StatusCodePagesOptions()
{
HandleAsync = (ctx) =>
{
if (ctx.HttpContext.Response.StatusCode == 404)
{
throw new RouteNotFoundException("Route not found");
}
return Task.FromResult(0);
}
})
RouteNotFoundException
public class RouteNotFoundException : Exception
{
public RouteNotFoundException()
: base()
{
}
public RouteNotFoundException(string message)
: base(message)
{
}
}
ApiExceptionFilterAttribute
public class ApiExceptionFilterAttribute : ExceptionFilterAttribute
{
private readonly IDictionary<Type, Action<ExceptionContext>> _exceptionHandlers;
public ApiExceptionFilterAttribute()
{
// Register known exception types and handlers.
_exceptionHandlers = new Dictionary<Type, Action<ExceptionContext>>
{
{ typeof(RouteNotFoundException), HandleNotFoundException }
};
}
public override void OnException(ExceptionContext context)
{
HandleException(context);
base.OnException(context);
}
private void HandleException(ExceptionContext context)
{
Type type = context.Exception.GetType();
if (_exceptionHandlers.ContainsKey(type))
{
_exceptionHandlers[type].Invoke(context);
return;
}
HandleUnknownException(context);
}
private void HandleNotFoundException(ExceptionContext context)
{
var exception = context.Exception as RouteNotFoundException;
var details = new ProblemDetails()
{
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.4",
Title = "The specified resource was not found.",
Detail = exception.Message
};
context.Result = new NotFoundObjectResult(details);
context.ExceptionHandled = true;
}
private void HandleUnknownException(ExceptionContext context)
{
var details = new ProblemDetails
{
Status = StatusCodes.Status500InternalServerError,
Title = "An error occurred while processing your request.",
Type = "https://tools.ietf.org/html/rfc7231#section-6.6.1"
};
context.Result = new ObjectResult(details)
{
StatusCode = StatusCodes.Status500InternalServerError
};
context.ExceptionHandled = true;
}
}
ResponseWrapperMiddleware
public class ResponseWrapperMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ResponseWrapperMiddleware> _logger;
public ResponseWrapperMiddleware(RequestDelegate next, ILoggerFactory loggerFactory)
{
_next = next ?? throw new ArgumentNullException(nameof(next));
_logger = loggerFactory?.CreateLogger<ResponseWrapperMiddleware>() ?? throw new ArgumentNullException(nameof(loggerFactory));
}
public async Task Invoke(HttpContext httpContext)
{
try
{
var currentBody = httpContext.Response.Body;
using (var memoryStream = new MemoryStream())
{
//set the current response to the memorystream.
httpContext.Response.Body = memoryStream;
await _next(httpContext);
//reset the body
httpContext.Response.Body = currentBody;
memoryStream.Seek(0, SeekOrigin.Begin);
var readToEnd = new StreamReader(memoryStream).ReadToEnd();
var objResult = JsonConvert.DeserializeObject(readToEnd);
var result = CommonApiResponse.Create((HttpStatusCode)httpContext.Response.StatusCode, objResult, null);
await httpContext.Response.WriteAsync(JsonConvert.SerializeObject(result));
}
}
catch (Exception ex)
{
if (httpContext.Response.HasStarted)
{
_logger.LogWarning("The response has already started, the http status code middleware will not be executed.");
throw;
}
return;
}
}
}
// Extension method used to add the middleware to the HTTP request pipeline.
public static class ResponseWrapperMiddlewareExtensions
{
public static IApplicationBuilder UseResponseWrapperMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<ResponseWrapperMiddleware>();
}
}
Generic Response Model
public class CommonApiResponse
{
public static CommonApiResponse Create(HttpStatusCode statusCode, object result = null, string errorMessage = null)
{
return new CommonApiResponse(statusCode, result, errorMessage);
}
public string Version => "1.2.3";
public int StatusCode { get; set; }
public string RequestId { get; }
public string ErrorMessage { get; set; }
public object Result { get; set; }
protected CommonApiResponse(HttpStatusCode statusCode, object result = null, string errorMessage = null)
{
RequestId = Guid.NewGuid().ToString();
StatusCode = (int)statusCode;
Result = result;
ErrorMessage = errorMessage;
}
}
How to handle the error if the route is not found and capture the error in the generic model? What is the workaround for this case?

How Do You Add ILogger To GlobalErrorHandling In .net 5 Web Api

I have my exception hander:
public class ApplicationExceptionHandler :ExceptionFilterAttribute
{
public ApplicationExceptionHandler()
{
}
public override void OnException(ExceptionContext context)
{
ApiError apiError = null;
switch (context.Exception)
{
case TaxiNotFoundException:
apiError = new ApiError(context.Exception.Message);
context.Result = new ObjectResult(apiError) {StatusCode = StatusCodes.Status404NotFound };
break;
default:
context.Result = new BadRequestResult();
break;
}
base.OnException(context);
}
}
public class ApiError
{
public string Message { get; set; }
public ApiError(string message)
{
Message = message;
}
public override string ToString()
{
return JsonConvert.SerializeObject(this);
}
}
[ApplicationExceptionHandler]
public class TaxisController : ControllerBase
{ etc....
How do I use the ILogger in the exception handler. If I make a constructor:
public ApplicationExceptionHandler(ILogger<ApplicationExceptionHandler> logger)
{
this.logger = logger;
}
In my controller I receive an compiler exception:
[ApplicationExceptionHandler (ILogger < ApplicationExceptionHandler > logger)]
Logger is a type which is not valid in this context. How do you add a ILogger to an exception handler?
Ok the syntax on the controller attribute:
[TypeFilter(typeof(ApplicationExceptionHandler))]
And the Exception handler:
public class ApplicationExceptionHandler :ExceptionFilterAttribute
{
private readonly ILogger<ApplicationExceptionHandler> logger;
public ApplicationExceptionHandler(ILogger<ApplicationExceptionHandler> logger)
{
this.logger = logger;
}

ASP.NET Core WebAPI Catch Model Binding Exceptions

I'm struggling to find a way to catch an exception thrown by a model property (actually by its type struct), which must be bound to a POST request body data.
I have a general scenario where I need to treat very specific data types, so I'm using structs to validate them accordingly each case.
Despite of the following codes are just drafts, all suggestions are very welcome!
So the following is an example of a Controller:
[ApiController]
[TypeFilter(typeof(CustomExceptionFilter))]
public class OrdersController : ControllerBase
{
public OrdersController(ILogger<OrdersController> logger, IDataAccess dataAccess)
{
_dataAccess = dataAccess;
_logger = logger;
}
private readonly IDataAccess _dataAccess;
private readonly ILogger<OrdersController> _logger;
[EnableCors]
[Route("api/[controller]/Sales")]
[HttpPost]
public async Task<ActionResult> PostSaleAsync(
[FromBody] SaleOrder saleOrder)
{
try
{
Guid saleOrderId = Guid.NewGuid();
saleOrder.SaleOrderId = saleOrderId;
foreach (SaleOrderItem item in saleOrder.items)
item.SaleOrderId = saleOrderId;
OrderQuery query = new OrderQuery(_dataAccess);
await query.SaveAsync(saleOrder);
_dataAccess.Commit();
var response = new
{
Error = false,
Message = "OK",
Data = new
{
SaleOrderId = saleOrderId
}
};
return Ok(response);
}
catch (DataAccessException)
{
_dataAccess.Rollback();
//[...]
}
//[...]
}
}
and an example of a model, Order, and a struct, StockItemSerialNumber:
public class SaleOrder : Order
{
public Guid SaleOrderId { get => OrderId; set => OrderId = value; }
public Guid CustomerId { get => StakeholderId; set => StakeholderId = value; }
public Guid? SellerId { get; set; }
public SaleModelType SaleModelType { get; set; }
public SaleOrderItem[] items { get; set; }
}
public class SaleOrderItem : OrderItem
{
public Guid SaleOrderId { get; set; }
public StockItemSerialNumber StockItemSerialNumber { get; set; }
//[JsonConverter(typeof(StockItemSerialNumberJsonConverter))]
//public StockItemSerialNumber? StockItemSerialNumber { get; set; }
}
public struct StockItemSerialNumber
{
public StockItemSerialNumber(string value)
{
try
{
if ((value.Length != 68) || Regex.IsMatch(value, #"[^\w]"))
throw new ArgumentOutOfRangeException("StockItemSerialNumber");
_value = value;
}
catch(RegexMatchTimeoutException)
{
throw new ArgumentOutOfRangeException("StockItemSerialNumber");
}
}
private string _value;
public static implicit operator string(StockItemSerialNumber value) => value._value;
public override string ToString() => _value;
}
I would like to catch ArgumentOutOfRangeException thrown by StockItemSerialNumber struct and then return a response message informing a custom error accordingly.
Since this exception is not catch by the try...catch block from Controller, I've tried to build a class that extends IExceptionFilter and add as a filter:
public class CustomExceptionFilter : IExceptionFilter
{
private readonly IWebHostEnvironment _hostingEnvironment;
private readonly IModelMetadataProvider _modelMetadataProvider;
public CustomExceptionFilter(
IWebHostEnvironment hostingEnvironment,
IModelMetadataProvider modelMetadataProvider)
{
_hostingEnvironment = hostingEnvironment;
_modelMetadataProvider = modelMetadataProvider;
}
public void OnException(ExceptionContext context)
{
context.Result = new BadRequestObjectResult(new {
Error = false,
Message = $"OPS! Something bad happened, Harry :( [{context.Exception}]."
});
}
}
Startup.cs :
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(options =>
// [...]
});
services.AddTransient<IDataAccess>(_ => new DataAccess(Config.DBCredentials));
services.AddControllers(options => options.Filters.Add(typeof(CustomExceptionFilter)));
}
But that approach also doesn't work, I mean, the ArgumentOutOfRangeException remains not being catch and crashes the execution during a POST request.
Finally, here's an example of a JSON with request data:
{
"CustomerId":"fb2b0555-6d32-404b-b2f0-50032a7e0f59",
"SellerId":null,
"items": [
{
"StockItemSerialNumber":"22B6E75510AB459B8DB2874F20C722B6F3DC19C6E474337D5F73BB87699E9A1001"
}, // Invalid
{
"StockItemSerialNumber":"022B6E755122B659B8DB2874F20C780030F3DC19C6E47465AS1673BB87699E9A1001"
} // Valid
]
}
So I appreciate any help or suggestion! Thanks!

.Net Core Channel in Background Tasks

I want using chanel in backgroundservice, but I have this error when run my code, what I need to do.
Sorry for bad english
Unable to resolve service for type
'System.Threading.Channels.ChannelReader`1[SendMailChanel]'
while attempting to activate 'SendEmailService'
public class SendMailChanel
{
public List<SendMail> SendMails { get; set; }
public List<string> MailTos { get; set; }
}
public class SendEmailService: BackgroundService
{
private readonly ChannelReader<SendMailChanel> _channel;
public HostedService(
ChannelReader<SendMailChanel> channel)
{
_channel = channel;
}
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
await foreach (var item in _channel.ReadAllAsync(cancellationToken))
{
try
{
// do your work with data
}
catch (Exception e)
{
}
}
}
}
[ApiController]
[Route("api/data/upload")]
public class UploadController : ControllerBase
{
private readonly ChannelWriter<SendMailChanel> _channel;
public UploadController (
ChannelWriter<SendMailChanel> channel)
{
_channel = channel;
}
public async Task<IActionResult> Upload([FromForm] FileInfo fileInfo)
{
SendMailChanel mailChanel = new SendMailChanel();
mailChanel.SendMails = lstSendMail;
mailChanel.MailTos = lstEmailTo;
await _channel.WriteAsync(mailChanel);
return Ok();
}
}
Startup.cs
services.AddHostedService<SendEmailService>();
follow this guide
https://flerka.github.io/personal-blog/2020-01-23-communication-with-hosted-service-using-channels/
services.AddHostedService<SendEmailService>();
services.AddSingleton<Channel<SendMailChanel>>(Channel.CreateUnbounded<SendMailChanel>(new UnboundedChannelOptions() { SingleReader = true }));
services.AddSingleton<ChannelReader<SendMailChanel>>(svc => svc.GetRequiredService<Channel<SendMailChanel>>().Reader);
services.AddSingleton<ChannelWriter<SendMailChanel>>(svc => svc.GetRequiredService<Channel<SendMailChanel>>().Writer);

Writing a generic ExceptionFitler in asp.net core 2.0

Below is my implementation. I have written a CustomExceptionFilterAttribute which inherits from ExceptionFilterAttribute. Each error is placed inside a if and else to generate proper result. What I would like to do is create a callback function so that I can remove if and else block and error can be handled in more generic way.
public class HostedServicesController : BaseController
{
public IActioResult Index()
{
throw new NotFoundInDatabaseException("Error in Index Controller");
}
}
public class NotFoundInDatabaseException : Exception
{
public NotFoundInDatabaseException(string objectName, object objectId) :
base(message: $"No {objectName} with id '{objectId}' was found")
{
}
}
public class CustomExceptionFilterAttribute :ExceptionFilterAttribute
{
private SystemManager SysMgr { get; }
public CustomExceptionFilterAttribute(SystemManager systemManager)
{
SysMgr = systemManager;
}
public override void OnException(ExceptionContext context)
{
var le = SysMgr.Logger.NewEntry();
try
{
le.Message = context.Exception.Message;
le.AddException(context.Exception);
var exception = context.Exception;
if (exception is NotFoundInDatabaseException)
{
le.Type = LogType.ClientFaultMinor;
context.Result = new NotFoundObjectResult(new Error(ExceptionCode.ResourceNotFound, exception.Message));
}
else if (exception is ConfigurationException)
{
le.Type = LogType.ErrorMinor;
context.Result = new BadRequestObjectResult(new Error(ExceptionCode.NotAuthorised, exception.Message));
}
else
{
le.Type = LogType.ErrorSevere;
context.Result = new InternalServerErrorObjectResult(new Error(ExceptionCode.Unknown, exception.Message));
}
le.AddProperty("context.Result", context.Result);
//base.OnException(context);
}
finally
{
Task.Run(() => SysMgr.Logger.LogAsync(le)).Wait();
}
}
}
You could use a Dictionary with a custom type for this. Think of something like this:
public class ErrorHandlerData
{
public LogType LogType { get; set; }
public string ExceptionCode { get; set; } // not sure if string
}
public class CustomExceptionFilterAttribute :ExceptionFilterAttribute
{
private static Dictionary<Type, ErrorHandlerData> MyDictionary = new Dictionary<Type, ErrorHandlerData>();
static CustomExceptionFilterAttribute()
{
MyDictionary.Add(typeof(NotFoundInDatabaseException), new ErrorHandlerData
{
LogType = LogType.ClientFaultMinor,
ExceptionCode = ExceptionCode.ResourceNotFound
};
//general catch-all
MyDictionary.Add(typeof(Exception), new ErrorHandlerData
{
LogType = LogType.ErrorSevere,
ExceptionCode = ExceptionCode.Unknown
};
}
So you can then use it like this:
public override void OnException(ExceptionContext context)
{
var le = SysMgr.Logger.NewEntry();
try
{
le.Message = context.Exception.Message;
le.AddException(context.Exception);
var exception = context.Exception;
var exeptionType = exception.GetType();
var errorHandlerData = MyDictionary.ContainsKey(exceptionType) ?
MyDictionary[exceptionType] : MyDictionary[typeof(Exception)];
le.Type = errorHandlerData.LogType;
context.Result = new NotFoundObjectResult(new Error(errorHandlerData.ExceptionCode, exception.Message));
le.AddProperty("context.Result", context.Result);
}
finally
{
Task.Run(() => SysMgr.Logger.LogAsync(le)).Wait();
}
}

Categories

Resources