7

EDIT:

I just realized that SaveChangesAsync returning 0 doesn't mean that it failed, entity framework will always throw exception when something fails so the check if SaveChanges == 0 is redundant! Save changes should always return 1 in the example below, if something fails then exception will be thrown.

However there are cases when something else is used and it's not entity framework so is this question for.


Servers can fail, when putting all my data access code in controllers I can handle it this way:

[HttpPost]
public async Task<ActionResult<Item>> CreateAsync([FromBody] Item item)
{
    await _dbContext.AddAsync(item);
    if (await _dbContext.SaveChangesAsync() == 0)
    {
        return StatusCode(StatusCodes.Status500InternalServerError);
    }
    return CreatedAtAction(nameof(GetAsync), new { id = item.Id }, item);
}

How should I handle it when my data access is encapsulated in a service layer?

public class ItemsService
{
    public async Task<Item> CreateAsync(Item item)
    {
        await _dbContext.AddAsync(item);
        if (await _dbContext.SaveChangesAsync() == 0)
        {
            return null;
        }
        return item;
    }
}

Then it would be used like that:

[HttpPost]
public async Task<ActionResult<Item>> CreateAsync([FromBody] Item item)
{
   // model state validation skipped for the sake of simplicity,
   // that would return BadRequest or some more valuable information
    var item = await _itemsService.CreateAsync(item);
    if (item == null)
    {
        return StatusCode(StatusCodes.Status500InternalServerError);
    }
    return CreatedAtAction(nameof(GetAsync), new { id = item.Id }, item);
}

Maybe this works for fine creating because there are only 2 status codes but let's consider updating where there can be more than 2 possible errors like:

  • Not found (404)
  • Internal server error(500)
  • Ok (200)

Code without services:

[HttpPut("{id}")]
public async Task<ActionResult<Item>> UpdateAsync(int id, [FromBody] Item itemToUpdate)
{
    var item = await _dbContext.Items.FindAsync(id);
    if (item == null)
    {
        return NotFound();
    }

    // update item with itemToUpdate
    //...
    await _dbContext.Update(item);
    if (await _dbContext.SaveChangesAsync() == 0)
    {
        return StatusCode(StatusCodes.Status500InternalServerError);
    }
    return item;
}

Now with services this can't be properly handled:

public class ItemsService
{
    public async Task<Item> UpdateAsync(Item updateItem)
    {
        var item = await _dbContext.Items.FindAsync(id);
        if (item == null)
        {
            return null;
        }
        //change some properties and update
        //...
        _dbContext.Items.Update(item);
        if (await _dbContext.SaveChangesAsync() == 0)
        {
            // what now?
        }
        return item;
    }
}

because it always returns null and there's no way to tell if the item was not found or the saving failed.

How can I handle it properly?

Note: I didn't add DTO or anything like that to keep this example simple.

Konrad
  • 6,385
  • 12
  • 53
  • 96
  • 1
    Have you thought about Exceptions that bubble up the layers instead of return values for hardware/io/transportation specific problems like "internet/server not available", "disk full" – k3b May 16 '18 at 11:02
  • 1
    @k3b Do you mean wrapping code in try-catch? Or what exactly do you mean? – Konrad May 16 '18 at 11:04

3 Answers3

3

Your service is responsible for catching all the exceptions it knows how to handle and handles those. All the other exceptions should be reported throught some kind of IServiceResult<T> providing both bool IsSuccessful and AggregatedException Exceptions { get; }. So rather than throw you let the highest level to decide how to react. It's actually the best approach from many points of view, including: purity of your functions (which is a key concept for easy parallezation, isn't it important for server?), clean and maintanable code (consumers of the service should not know/care that much: they just verify whether result is successful and consume the result; otherwise dig into aggreaged exception; might make sense to implement your own type of exception providing methods suitable for needs of your project), ability to procceed on higher-order-functions, which are responsible for actual error handling; take rX as a popular example: .OnError(Action<Exception> handler). There are much more than this, however I don't like to make the answer longer that it needs to be.

As a side note. Make sure you read an introductory lever articles about Maybe and Either monads from Haskell programming language; in case you'd prefer F# more, you can try to seek for Some and Either respectively. There you can find useful methods like .Map(...) and see nice ways to combine/aggregate those in efficient way.

Good luck.

Zazaeil
  • 3,900
  • 2
  • 14
  • 31
  • How should AggregatedException look like and how should it be filled? – Konrad May 16 '18 at 11:31
  • Untortunately I don't know much about Haskell nor F# as I never used them, I know about reactive programming though if this is what you meant by `rX` – Konrad May 16 '18 at 11:32
  • 1
    @Konrad `AggException` is a standard `.NET` class. It is used everywhere in multithreaded programming due to essence of concurency and parallel computations. In case you're not familiar (yet), it definitely makes sense to catch a moment and try it out. However, right now you might skip those as kind of "advanced" approaches; the solution I've recommended above would be fully compatible with those ideas and you are free to get back to them ASAP or never. – Zazaeil May 16 '18 at 11:35
2

The code with the service is far better. The controller sould delegate business logic to the service layer.

To make your implementation of the service layer more better, you should:

  • throw custom exceptions when something wrong happened in your service. Example : throw ItemNotFoundExcption instead of returning null. ItemUpdateException when updates failed.
  • in your controller you need to catch all possible custom exceptions that might happenned and do what need to be done like returning NotFound() for ItemNotFoundException and BadRequest() for ItemUpdateException.
  • the latter point can be made through custom action filter which helps you make your controller's actions more lean.
CodeNotFound
  • 22,153
  • 10
  • 68
  • 69
  • Service shouldn't even know that `.BadRequest()` exists. Service should now hot to.. serve ;) Having a service aware of responses is definitely wrong segregation of responsibilities leading to a poor overall architecture. In case service can't serve due to any factor, this particular factor has to be reported to a caller. That's it; then **assumed caller rather than service** makes a decision what to do next. – Zazaeil May 16 '18 at 11:14
  • 1
    @SerejaBogolubov At which line I said service do `BadRequest()` ? Dude re-read my answer. Service throw only custom exceptions which are catched by controller's action. Then action can interpret the custom exception to return BadRequest, NotFound or anything that belong to that layer. – CodeNotFound May 16 '18 at 11:17
  • 1
    Indeed. Then the purity is only question, however I rollback my -1 such that it becomes +1 )))) – Zazaeil May 16 '18 at 11:18
0

It's actually a pretty big topic and there are dozens of options to do it.

I prefer to throw exceptions. You can create your own classes for that or use .net ones. You can have NotFoundException, InternalServerError and etc. Each class can have StatusCode and Message field or whatever you want. Next you need to implement exception filter for asp.net which will handle all exceptions and return response with status code that can be retrieved from exception class.

Another option maybe simpler. You can create a class that will contain StatusCode and object as a result so you will return it from your methods. It's a way to return more then just null or object from your methods.

Few good articles: Exceptions or error codes

https://www.hanselman.com/blog/GoodExceptionManagementRulesOfThumb.aspx

http://codebetter.com/karlseguin/2006/04/05/understanding-and-using-exceptions/

Iaroslav
  • 295
  • 1
  • 10