Need Help making dotnet controllers DRY

Viewed 88

Need some help making my controllers DRY using the ASP.NET core

So, basically, I have an issue keeping my controllers non-fat and DRY.

Basically, let's say I have 4 controllers : controllerA, controllerB ... controllerD - the general structure of the code for each controller looks like this (pseudocode)

class controllerA 
{
  IActionResult Get(string ids)
  {
    //do validation on ids - if invalid return 404

    // IEnumerable<obj> results= aservice.GetResource(ids)

    -------------------------------------------------------------------------

    //Logging of results

    //logic which looks at results and determines whether to return a 404 or a 200

    //return results
  }
}             

The logic after the dotted line is essentially repeated in 4 controllers - how can I make the controllers more DRY.

I have read about different solutions including using a DI service, a helper class, extension methods for the Controller type or an abstract class that these controllers can inherit from that extends the .NET controller class.

I am writing production code so would like to seek out a clean solution which is also best practice!

Thanks!

1 Answers

I would recommend to use MediatR nuget. You can implement IPipelineBehavior<,> for each required step, in your example it will be:

  • Validation of ids
  • Logging

Your code will look like:

public async Task<IActionResult> Get(CustomCommand request)
{
   var result = await _mediator.Send(request);
   // return handled result
}

If you define command as parameter of action it will reduce code more, something like this

public async Task<IActionResult> Get(string ids)
{
   // under the hood it will execute (depends on implementation):
   // 1. validation of ids for the command
   // 2. execute main logic which implemented in IRequestHandler<>
   // 3. logging
   // 4. return result
   var result = await _mediator.Send(new CustomCommand(ids));
   // return handled result
}

About results handling, there are two common ways functional programming or using exceptions.

Using exceptions

  1. Create customer exceptions for your service, for example, NotFoundException, BadRequestException (better to call it something meaningful like ValidationException)
  2. Throw them in cases where you need to stop execution and return unsuccessful result (for example, validation was failed)
  3. Add global exception filter to asp.net which will handle this exceptions depends on it's type and it might use some custom data from it

Functional programming

Instead of simply returning response you will return Result object within the response, simple implemented of Result<T>:

public class Result<TValue>
{
   public TValue Value { get; set; }
   public bool IsSuccess { get; set; }
   // you can add also something like 'ErrorCode' to specify how to handle
   // failed result, for instance if ErrorCode is 400 (it can be readable
   // string as well) you will return BadRequest()

}

Finally you can define in base controller class method to execute the logic and handle results

//... inside your base controller
public async Task<ActionResult<TResponse>> ExecuteAsync<TResponse>(IRequest<Result<TResponse>> request)
{
   Result<TResponse> result = await Mediator.Send(request);
   if(result.IsSuccess) return Ok(result.Value);
   return result.ErrorCode switch
   {
       "some_error_code" => Conflict(result.ErrorMessage),
       "other_code" => NotFound(),
       _ => BadRequest(result.ErrorMessage)
   };
}
//...

so the action code will be:

public Task<IActionResult> Get(CustomCommand request) => ExecuteAsync(request); 
Related