Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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!

like image 391
Luiz Avatar asked Sep 22 '26 06:09

Luiz


1 Answers

According to the docs for ASP.NET Core for .NET 6 you should now be able to use exception filters to handle exceptions thrown during model binding:

[Exception filters] handle unhandled exceptions that occur in Razor Page or controller creation, model binding, action filters, or action methods. [They] do not catch exceptions that occur in resource filters, result filters, or MVC result execution.

I have tested it and it works for me, using this class:

public class UnhandledExceptionFilter: IExceptionFilter
{
  /// <inheritdoc/>
  public void OnException(ExceptionContext argContext) {
    // we have nothing to do if no exception information is provided (this should not happen).
    if(argContext.Exception is null) return;

    // we also have nothing to do if an exception was thrown but has been handled.
    if(argContext.ExceptionHandled) return;

    // at this point we know that an exception was thrown and it hasn't been handled
    // so return our standard error response and mark the exception as handled so that
    // ASP.NET doesn't return an Internal Server Error response.
    argContext.Result = new OkObjectResult(new BaseResponse(argContext.Exception));
    argContext.ExceptionHandled = true;
  }
}

In my code the BaseResponse is class that all API response objects inherit from. When it is given an Exception object it sets all the relevant fields in the response to indicate that an error has occurred.

like image 193
Eric Mutta Avatar answered Sep 24 '26 19:09

Eric Mutta



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!