1

I'm working on a new microservice and would like the to put an interface on the API for my clients.

For example.
I have a customer service with ApiController and a method:

[HttpGet("api/v1/[action]")]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
[ProducesResponseType((int)HttpStatusCode.NotFound)]
[ProducesResponseType(typeof(List<CustomerDto>), (int)HttpStatusCode.OK)]
public async Task<IActionResult> GetCustomers()
{
    Log.Logger.Information($"Calling GetCustomers()");
    var ret = await _db.Customer.GetListAsync<CustomerDto>();
    Log.Logger.Information($"GetCustomers()=>{ret?.Count}");
    if (ret == null)
    {
        return NotFound();
    }
        return Ok(ret);
    }

In my client code I have a service interface that abstracts the HTTP calls to the above service. So my client using this service can simply inject a ICustomerService and call GetCustomers().

This is all working great, however would I really want is to put an the same ICustomerService interface on the API controller, so I can catch any issues at compile time. The problem is that ICustomerService.GetCustomer returns List<CustomerDto> and the controller returns IActionResult.

Task<List<CustomerDto>> GetCustomers();

VS

public async Task<IActionResult> GetCustomers(){}

Is there a way to enforce the ICustomerService interface on the API controller at build time?

Azametzin
  • 5,223
  • 12
  • 28
  • 46
  • Make your own class that implements `IActionResult`; if that is a generic type for a result set, you can specify that. But then you have to reimplement the built-in helper functions you are using. `OK` already takes in a generic type, but `NotFound` does not, so you can't use that method if you want to return a result every time. E.g., if your return type was `ActionResult>`, returning `NotFound` should not compile. – ps2goat Mar 30 '20 at 16:13
  • I've found [swashbuckle](https://learn.microsoft.com/en-us/aspnet/core/tutorials/getting-started-with-swashbuckle?view=aspnetcore-3.1&tabs=visual-studio) great for api client generation, you can annotate endpoints with output types and use the resulting schema that's created for client generation. See [this](https://stackoverflow.com/questions/45182448/swashbuckle-swagger-documentation-of-returned-response) question. – Oliver Mar 30 '20 at 16:23
  • 1
    I don't think what you're striving for is a good idea. The interface is for the client to represent how the server works. Instead of generating your own interface and client, you can use Swashbuckle to generate it, or switch to using gRPC (where they rely on a common contract) instead of an HTTP API. – mason Mar 30 '20 at 16:24
  • You can use a generic interface and implement like this MyClass: ICustomerService>> and on the COntroller: ICustomerService> – Jonathan Alfaro Mar 30 '20 at 20:33
  • You can also try to return Task> from your Controller – Jonathan Alfaro Mar 30 '20 at 20:34

1 Answers1

1

I had this same question and looked around and couldn't find anything so I ended up designing my own way of doing this since the answers here weren't good enough.

What I did was:

  1. Create the interface:
public interface MyApi {
    LoginResponse Login(LoginRequest request);
}

C# Web API Controllers need to respond with an ActionResult type. Therefore, just slapping the interface on a controller is not good enough. We need to implement our logic but still provide a way to convert non-optimal paths into ActionResult types. We can do this with exceptions.

  1. Create a custom abstract exception type:
public abstract class ActionResultException : Exception
{
    // This is a generic object I return to every method call.
    // I fill my `ActionResults` with this object. You can remove this if you don't need it
    public NetworkResponse response;

    // This method will convert the exception into an `ActionResult`
    public abstract ActionResult ToActionResult();
}


// Create an Impl of the abstract exception above for each `ActionResult` type you need.
public class NotFoundException : ActionResultException
{
    public override ActionResult ToActionResult()
    {
        // This implementation returns the `NotFound` ActionResult Impl:
        return new NotFoundObjectResult(response);
    }
}

Note: For other ActionResult types, they are in the format <Name>ObjectResult for example, NotFoundObjectResult, OkObjectResult, ConflictObjectResult, etc..

  1. Now that we have created exceptions, we need to handle them. Create a new Middleware that will handle any thrown exceptions in your application (if you don't have one already).
public class ExceptionResponseMiddleware : ExceptionFilterAttribute {

    public async override Task OnExceptionAsync(ExceptionContext context)
    {
        await HandleException(context);
        await base.OnExceptionAsync(context);
    }

    public async override void OnException(ExceptionContext context)
    {
        await HandleException(context);
        base.OnException(context);
    }

    private async Task HandleException(ExceptionContext context)
    {
        // If the result was one of our specific exception types, we can instead cast it to the expected result.
        if (context.Exception is ActionResultException exception)
        {
            context.Result = exception.ToActionResult();
            context.ExceptionHandled = true;
            return;
        }
}

I also added some database tracking here so that I can tracking any exceptions that are not "expected" exceptions here. Useful to keep stack traces around that others users might be producing in a live environment.

  1. Make your controller implement your interface, but throw instead of returning ActionResult types:
[ApiController]
public class LoginController : ControllerBase, MyApi {

    [HttpPost]
    public async Task<LoginResponse> Login(LoginRequest request) {
        // Do logic here
        // Throw exceptions instead of returning `ActionResult`
    }
}
  1. Register your Exception handler as a middleware service in Program.cs:
// Add Exception Handling Filter
builder.Services.AddControllers(options =>
{
    options.Filters.Add<ExceptionResponseMiddleware>();
});

This allows you to implement your network interface directly in your controllers!

Hopefully, this helps someone in the future. Wish I had known this before I got this far in my project :)

R10t--
  • 803
  • 7
  • 16