Rahul Dev
Rahul Dev

Reputation: 141

What is the best possible way to send custom error responses in .net core web api

I'm making a .net Core WebApi using .Net Core 2.2. The API is ready but the failure message and response is where I'm stuck at.

Right now, I'm getting respose like below

json

{
    "empId":1999,
    "empName":"Conroy, Deborah",
    "enrollmentStatus":true,
    "primaryFingerprintScore":65,
    "secondaryFingerprintScore":60,
    "primaryFingerprint":null,
    "secondaryFingerprint":null,
    "primaryFingerprintType":null,
    "secondaryFingerprintType":null}
}

I created a json formatter class and wrote the below code

public class SuperJsonOutputFormatter : JsonOutputFormatter
{
    public SuperJsonOutputFormatter(
        JsonSerializerSettings serializerSettings,
        ArrayPool<char> charPool) : base(serializerSettings, charPool)
    {
    } 

    public override async Task WriteResponseBodyAsync(
        OutputFormatterWriteContext context,
        Encoding selectedEncoding)
    {

        if (context == null)
            throw new ArgumentNullException(nameof(context));
        if (selectedEncoding == null)
            throw new ArgumentNullException(nameof(selectedEncoding));
        using (TextWriter writer =
            context.WriterFactory(
                context.HttpContext.Response.Body,
                selectedEncoding))
        {

            var rewrittenValue = new
            {
                resultCode = context.HttpContext.Response.StatusCode,
                resultMessage =
                ((HttpStatusCode)context.HttpContext.Response.StatusCode)
                    .ToString(),
                result = context.Object
            };

            this.WriteObject(writer, rewrittenValue);
            await writer.FlushAsync();
        }
    }

I expect all the error codes to be sent as generic error messages like the JSON below.

FOR STATUS OKAY:

{
    "status" : True,
    "error"  : null,
    "data" : {
        {
            "empId":1999,
            "empName":"Conroy, Deborah",
            "enrollmentStatus":true,
            "primaryFingerprintScore":65,
            "secondaryFingerprintScore":60,
            "primaryFingerprint":null,
            "secondaryFingerprint":null,
            "primaryFingerprintType":null,
            "secondaryFingerprintType":null}
        }
    }   
}

FOR OTHER STATUS LIKE 404, 500, 400, 204

{
    "status" : False,
    "error"  : {
        "error code" : 404,
        "error description" : Not Found
    },
    "data" : null   
}

Upvotes: 2

Views: 2328

Answers (1)

itminus
itminus

Reputation: 25380

I expect all the error codes to be sent as generic error messages like the JSON below

You're almost there. What you need to do is enabling your SuperJsonOutputFormatter.

A Little Change to Your Formatter

Firstly, your formatter didn't return a json with the same schema as you want. So I create a dummy class to hold the information for error code and error description:

public class ErrorDescription{
    public ErrorDescription(HttpStatusCode statusCode)
    {
        this.Code = (int)statusCode;
        this.Description = statusCode.ToString();
    }

    [JsonProperty("error code")]
    public int Code {get;set;}
    [JsonProperty("error description")]
    public string Description {get;set;}
}

And change your WriteResponseBodyAsync() method as below:

    ...

    using (TextWriter writer = context.WriterFactory(context.HttpContext.Response.Body, selectedEncoding)) {
        var statusCode = context.HttpContext.Response.StatusCode;
        var rewrittenValue = new {
            status = IsSucceeded(statusCode),
            error = IsSucceeded(statusCode) ? null : new ErrorDescription((HttpStatusCode)statusCode),
            data  = context.Object,
        };
        this.WriteObject(writer, rewrittenValue);
        await writer.FlushAsync();
    }

Here the IsSucceeded(statusCode) is a simple helper method that you can custom as you need:

private bool IsSucceeded(int statusCode){
    // I don't think 204 indicates that's an error. 
    //     However, you could comment out it if you like
    if(statusCode >= 400 /* || statusCode==204 */ ) { return false; }
    return true;
}

Enable your Formatter

Secondly, to enable your custom Formatter, you have two approaches: One way is to register it as an global Formatter, the other way is to enable it for particular Controller or Action. Personally, I believe the 2nd way is better. So I create a Action Filter to enable your formatter.

Here's an implementation of the Filter that enables your custom formatter dynamically:

public class SuperJsonOutputFormatterFilter : IAsyncActionFilter{
    private readonly SuperJsonOutputFormatter _formatter;
    // inject your SuperJsonOutputFormatter service
    public SuperJsonOutputFormatterFilter(SuperJsonOutputFormatter formatter){
        this._formatter = formatter;
    }
    // a helper method that provides an ObjectResult wrapper over the raw object
    private ObjectResult WrapObjectResult(ActionExecutedContext context, object obj){
        var wrapper = new ObjectResult(obj);
        wrapper.Formatters.Add(this._formatter);
        context.Result= wrapper;
        return wrapper;
    }

    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        ActionExecutedContext resultContext = await next();
        // in case we get a 500
        if(resultContext.Exception != null && ! resultContext.ExceptionHandled){
            var ewrapper = this.WrapObjectResult(resultContext, new {});
            ewrapper.StatusCode = (int) HttpStatusCode.InternalServerError; 
            resultContext.ExceptionHandled = true;
            return;
        }
        else {
            switch(resultContext.Result){
                case BadRequestObjectResult b :      // 400 with an object
                    var bwrapper=this.WrapObjectResult(resultContext,b.Value);
                    bwrapper.StatusCode = b.StatusCode;
                    break;
                case NotFoundObjectResult n :        // 404 with an object
                    var nwrapper=this.WrapObjectResult(resultContext,n.Value);
                    nwrapper.StatusCode = n.StatusCode;
                    break;
                case ObjectResult o :                // plain object
                    this.WrapObjectResult(resultContext,o.Value);
                    break;
                case JsonResult j :                  // plain json
                    this.WrapObjectResult(resultContext,j.Value);
                    break;
                case StatusCodeResult s:             // other statusCodeResult(including NotFound,NoContent,...), you might want to custom this case 
                    var swrapper = this.WrapObjectResult(resultContext, new {});
                    swrapper.StatusCode = s.StatusCode;
                    break;
            }
        }
    }

}

And don't forget to register your formatter as a service :

    services.AddScoped<SuperJsonOutputFormatter>();

Finally, when you want to enable your formatter, just add a [TypeFilter(typeof(SuperJsonOutputFormatterFilter))] annotation for the controller or action.

Demo

Let's create an action method for Test:

[TypeFilter(typeof(SuperJsonOutputFormatterFilter))]
public IActionResult Test(int status)
{
    // test json result(200)
    if(status == 200){ return Json(new { Id = 1, }); }
    // test 400 object result
    else if(status == 400){ return BadRequest( new {}); } 
    // test 404 object result
    else if(status == 404){ return NotFound(new { Id = 1, }); }
    // test exception
    else if(status == 500){ throw new Exception("unexpected exception"); }
    // test status code result
    else if(status == 204){ return new StatusCodeResult(204); }

    // test normal object result(200)
    var raw = new ObjectResult(new XModel{
        empId=1999,
        empName = "Conroy, Deborah",
        enrollmentStatus=true,
        primaryFingerprintScore=65,
        secondaryFingerprintScore=60,
        primaryFingerprint = null,
        secondaryFingerprint= null,
        primaryFingerprintType=null,
        secondaryFingerprintType=null
    });
    return raw;
}

Screenshot:

enter image description here

Upvotes: 2

Related Questions