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:
- 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.
- 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..
- 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.
- 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`
}
}
- 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 :)
>`, returning `NotFound` should not compile.
– ps2goat Mar 30 '20 at 16:13> from your Controller
– Jonathan Alfaro Mar 30 '20 at 20:34