top of page

Ways to handle errors in .NET



Error handling is always an essential part of an application. When processing some business logic requests, it’s always necessary to let a user know whether the execution result succeeded or failed.


Let’s assume that a user sends you a request for an endpoint, and you have a service that processes the request, but it fails due to the business validation. The easiest way to handle the situation is to create a response model where you input the errors and the status for the execution result to find out whether it succeeded or failed.


I will give you a simple example.


Let’s assume that we have an ExampleController.cs where we have a Test endpoint.

[Controller]
[Route("api/example")]
public class ExampleController: ControllerBase {
    private readonly ExampleService _exampleService;
    
    public ExampleController(ExampleService exampleService) {
        _exampleService=exampleService;    
    }    
    
    [HttpPost]
    public async Task <IActionResult> Test() {
        var exampleResult = await _exampleService.CreateAsync();
        if (!exampleResult.IsSucceeded) {
            return BadRequest(exampleResult.Message);        
       }
       
       return Ok(exampleResult);    
    }
}

In the test method, we are calling the CreateAsync method that processes the request.

public class ExampleService {
    public async Task <ExampleResponse> CreateAsync() {
        var exampleResponse = new ExampleResponse();
        exampleResponse.IsSucceeded = false;
        exampleResponse.Message = "Example error message";
        return exampleResponse;    
    }
}

Let’s assume we have some business validations to make where one of them failed, the Controller will need to be notified about the error to return the appropriate status code to the caller. As you can see above, we’ve created an ExampleResponse class that contains all the necessary information for the Controller to return the status code.


But if our codebase grows, we will end up with a big ball of mud. It will also make the development harder.


Let’s take a look at another solution.


A better way to avoid repetitive code, we can create a generic class where we can separate data from the execution result status.


Here is an example of the Result class.

public class Result < T > {
    public T Data {
        get;
        set;    
    }
    public bool IsSucceeded {
        get;
        set;    
    }
    public string Message {
        get;
        set;    
    }
}

As you can see, the data model and the execution result are separated. This way, we don’t need to add extra properties to our data model. Also, we can create an abstract class for our services, where we can handle the creation of the result class.

public abstract class BaseService {
    public Result<T> Failed<T>() {
        return new Result<T>(isSucceeded: false);  
    }
    
    public Result<T> Succeeded<T> () {
        return new Result<T>(isSucceeded: true);  
    }
    
    public Result<T> NotFound<T>() {
        return new Result<T>(isSucceeded: false);  
    }
}

In the above example, we’ve created the base service that we can use in our previous service.

We can refactor the ExampleService

public class ExampleService : BaseService
{
    public async Task<Result<ExampleResponse>> CreateAsync(Guidid)    
    {
        if (id == Guid.Empty)        
        {
            return Failed<ExampleResponse>
            (/*You can include error messages here*/);        
        }
        return Succeeded<ExampleResponse>
        (/*You can add a data here*/);    
    }
}

Now you can easily add more validations and read the code.


But what if you want to use the existing method in another method? With the above solution, you will end up with a lot of unnecessary validations. Also, the result class is created for the Controllers to notify the client about the execution result. Once you use the existing method, you have to maintain it. First of all, you have to check it against any failures. If it failed, you have to pass the result errors to the current method. A better solution for the problem is to use exceptions.


There are many ways to implement the above solution. For example, you can throw an exception from a method or have an abstract class with appropriate methods. I prefer to use the abstract class; because it’s easier to read and use.


First of all, let’s refactor our base service.

public abstract class BaseService
{
    public T Failed<T>()    
    {
        throw new FailedException();    
    }
    
    public T NotFound<T>()    
    {
        throw new NotFoundException();    
    }
}

We’ve removed the Succeeded method from the service because we don’t have the result class. If it succeeds, we can return data directly. Also, I’m using generic methods here for readability. Everything else will stay the same in our ExampleService. You can handle exceptions either in a controller or in a middleware.



Source: Medium - George Machaidze


The Tech Platform

0 comments

Comments


bottom of page