How to change routing errors in .NET Core 3.1 API

Viewed 1330

I am porting an API from ASP .NET 4.7 to .NET CORE 3.1 and so far I have managed to get most of the work done, but I got stuck at a specific error. I have the following endpoint

    [HttpGet]
    [Route("contacts/{id}/")]
    public IActionResult GetContactByID(Guid? id)
    {
        if (!id.HasValue)
            return BadParameter("id");

        //Some irrelevant logic here
        Contact foundContact = GetContactFromRepository(id);
        return Ok(foundContact);
    }

By providing an invalid Guid ("contacts/6cb735b1-b2f0-45d1-a5b5-3a161a5b8c5eTESTINVALIDGUID") in the old API - the endpoint would be reached and the IF statement checking if ID has value would provide the expected error response format. Currently in CORE 3.1 when In identical request I am getting the following response in Postman:

"errors": {
    "id": [
        "The value '6cb735b1-b2f0-45d1-a5b5-3a1615555555asd' is not valid."
    ]
},
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "|90331587-428887e84126845c."

The execution never reaches the method. But the constructor of the controller gets called.

I have overwritten most of the error messages and I am porting multiple API versions in the same time ( with different responses for each ). But I cannot figure out where this error is coming from ( and how to handle it so the request actually reaches the controller method - so that I can return the correct response ).

3 Answers

In ASP.NET Core, if you specify the parameter type of Action Method as Guid and expect the route to get the Guid value, it should be a valid Guid value, other wise the route will not work. The .Net Core framework will not parse invalid value.

I have tested the following code in .Net Core 3.1:

    [HttpGet]
    [Route("sitemap/{id}/")]
    public IActionResult GetSitemap(Guid id)
    {
         // ToDo- Perform some operation
    }

Routes:

http://localhost:6003/api/sitemap/6cb735b1-b2f0-45d1-a5b5-3a1615555555 // Works fine, Valid Request

http://localhost:6003/api/sitemap/6cb735b1-b2f0-45d1-a5b5-3a1615555555aab //Invalid Guid, Bad Request

As an alternate to pass Guid, you can change the parameter type to string and handle the type-checking with in the action method:

        [HttpGet]
        [Route("sitemap/{id}/")]
        public IActionResult GetSitemap(string id)
        {
            if (id.Length == 36)
            {
                Guid value = Guid.Parse(id);
            }
            else
            {
                return BadRequest();
            }
         }

I figured it out ! I researched a bit more on ModelBinders and figured out that having a CustomModelBinder gets the job done.

public class GuidModelBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (bindingContext == null)
        {
            throw new ArgumentNullException(nameof(bindingContext));
        }

        var modelName = bindingContext.ModelName;

        // Try to fetch the value of the argument by name
        var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);

        if (valueProviderResult == ValueProviderResult.None)
        {
            bindingContext.Result = ModelBindingResult.Failed();
            return Task.CompletedTask;
        }


        Guid parsedGuid;
        if (Guid.TryParse(valueProviderResult.FirstValue, out parsedGuid))
        {
            bindingContext.Result = ModelBindingResult.Success(parsedGuid);
        }
        else
        {
            bindingContext.Result = ModelBindingResult.Failed();
        }
        return Task.CompletedTask;

    }
}

Always returning "Task.CompletedTask" makes sure that it will trigger the controller action even if ModelBindingResult is Failed. And on testing it works exactly as intended - Controller method gets assigned for Guid? Id - the value of Null.

In order to not have to add a ModelBinder attribute on each ActionMethod I created a CustomModelBinderProvider that makes sure the CustomBinder applies only to Nullable Guids(its what I need):

public class GuidModelBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        if (context.Metadata.ModelType == typeof(Guid?))
        {
            return new GuidModelBinder();
        }

        return null;
    }
}

To add the provider I put the following in Startup.cs

 services.AddControllers(op =>{
            op.ModelBinderProviders.Insert(0, new GuidModelBinderProvider());
        })
Related