Reputation: 2458
I'm trying to handle exception in services part in webapi. The goal I try to achieve is:
When service gets an error e.g. when didn't find the todo with certain id
controller should return custom json response with custom status code.
Because in my project controller and service are separated don't have any idea how to achieve it.
Thanks in advance.
Below my code:
in Services/TodoRepository.cs
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using test.Data;
using test.DTO;
using test.Interfaces;
using test.Models;
namespace test.Services
{
public class TodoRepository : ITodoRepository
{
private readonly DataContext _context;
public TodoRepository(DataContext context)
{
_context = context;
}
public async Task<IEnumerable<Todo>> GetTodosAsync()
{
return await _context.Todos.ToListAsync();
}
public async Task<Todo> GetTodoAsync(int id)
{
return await _context.Todos.FindAsync(id);
}
public async Task<Todo> AddTodoAsync(TodoInput input)
{
var todo = new Todo
{
Title = input.Title,
Description = input.Description,
IsDone = input.IsDone
};
await _context.Todos.AddAsync(todo);
return todo;
}
public async Task<Todo> SetTodoAsync(int id, TodoInput input)
{
var todo = await _context.Todos.FindAsync(id);
if (todo == null)
//
// CONTROLLER SHOULD RETURN CUSTOM
// JSON RESPONSE WITH CUSTOM STATUS CODE
// INSTEAD OF EXCEPTION WITH INTERNAL SERVER ERROR
//
throw new Exception("Could not find an item with this id");
todo.Title = input.Title;
todo.Description = input.Description;
todo.IsDone = input.IsDone;
todo.Updated = DateTime.Now;
_context.Todos.Update(todo);
return todo;
}
public async Task<Todo> DeleteTodoAsync(int id)
{
var todo = await _context.Todos.FindAsync(id);
if (todo == null)
//
// CONTROLLER SHOULD RETURN CUSTOM
// JSON RESPONSE WITH CUSTOM STATUS CODE
// INSTEAD OF EXCEPTION WITH INTERNAL SERVER ERROR
//
throw new Exception("Could not find an item with this id");
_context.Todos.Remove(todo);
return todo;
}
public async Task<bool> SaveAllAsync()
{
return await _context.SaveChangesAsync() > 0;
}
}
}
in Controllers/TodosController.cs
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using test.Data;
using test.DTO;
using test.Interfaces;
using test.Models;
namespace test.Controllers
{
[ApiController]
[Route("[controller]")]
public class TodosController : ControllerBase
{
private readonly DataContext _context;
private ITodoRepository _repository;
public TodosController(DataContext context, ITodoRepository repository)
{
_context = context;
_repository = repository;
}
[HttpGet]
public async Task<ActionResult<IEnumerable<Todo>>> GetTodos()
{
return Ok(await _repository.GetTodosAsync());
}
[HttpGet("{id}")]
public async Task<ActionResult<Todo>> GetTodo([FromRoute] int id)
{
return Ok(await _repository.GetTodoAsync(id));
}
[HttpPost]
public async Task<ActionResult<Todo>> AddTodo([FromBody] TodoInput input)
{
var todo = await _repository.AddTodoAsync(input);
if (!await _repository.SaveAllAsync())
return BadRequest("something gone wrong");
return Ok(todo);
}
[HttpPut("{id}")]
public async Task<ActionResult<Todo>> SetTodo([FromRoute] int id, [FromBody] TodoInput input)
{
var todo = await _repository.SetTodoAsync(id, input);
if (!await _repository.SaveAllAsync())
return BadRequest("something gone wrong");
return Ok(todo);
}
[HttpDelete("{id}")]
public async Task<ActionResult<Todo>> DeleteTodo([FromRoute] int id)
{
var todo = await _repository.DeleteTodoAsync(id);
if (!await _repository.SaveAllAsync())
return BadRequest("something gone wrong");
return Ok(todo);
}
}
}
Upvotes: 1
Views: 73
Reputation: 196
So for your service class, you are going to want to have a try/catch that returns a BadResult object:
try
{
//TODO Test Case check
if(x != 1)
{
throw new Exception();
}
catch (Exception e)
{
return BadRequest("Could not find an item with this id");
}
Now for your controller on your ASP .Net Client, when you check the result from your Task that you kicked off to call your API:
var acctResult = responseAcctTask.Result;
if (acctResult.StatusCode == HttpStatusCode.BadRequest)
{
//log response status here..
ModelState.AddModelError(string.Empty, "Server error.");
}
You can at that point handle the error any way, I prefer to add to the modelState so I can access error info on my views.
Upvotes: 1
Reputation: 2820
Assuming that you will throw some specific exceptions in some methods while other you just want to catch as System.Exception maybe you can use this approach:
You can create Exception handling extension method (if you want to use Logger too just add ILogger parameter in the method and pass it from Startup.Configure):
public static class ExceptionHandler
{
/// <summary>
///
/// </summary>
/// <param name="app"></param>
public static void UseCustomExceptionHandler(this IApplicationBuilder app)
{
app.UseExceptionHandler(eApp =>
{
eApp.Run(async context =>
{
context.Response.StatusCode = 500;
context.Response.ContentType = "application/json";
var errorCtx = context.Features.Get<IExceptionHandlerFeature>();
if (errorCtx != null)
{
var ex = errorCtx.Error;
var message = "Unspecified error ocurred.";
if (ex is ValidationException)
{
var validationException = ex as ValidationException;
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
message = string.Join(" | ", validationException.Errors.Select(v => string.Join(",", v.Value)));
}
else if (ex is SomeCustomException)
{
var someCustomException = ex as SomeCustomException;
...
}
var jsonResponse = JsonConvert.SerializeObject(new ErrorResponse
{
TraceId = traceId,
Message = message
});
await context.Response.WriteAsync(jsonResponse, Encoding.UTF8);
}
});
});
}
}
And then you just register it in Startup Configure:
public void Configure(IApplicationBuilder app)
{
...
app.UseCustomExceptionHandler();
...
}
For different exceptions you can set different Status Code: context.Response.StatusCode = 404;
Upvotes: 2