The Result Pattern in ASP.NET Core Minimal APIs

Comments 0

Share to social media

ASP.NET Core Minimal APIs are a new (as of the end of 2024) way to build web APIs in ASP.NET Core. Minimal APIs are lightweight and easy to use, if you keep the handlers minimal.

In most API endpoints there is a need to tackle errors, which can introduce complications to the code. An API endpoint can return an error in many ways, such as internal server errors, bad requests, or not found errors.

In this article, I will show you the usual way of tackling errors in ASP.NET Core, and how it can be improved using the Result pattern. Traditionally, you bubble up errors in ASP.NET Core is by throwing exceptions. While this is a workable way of dealing with errors, it has its own problems.

To show the pattern, I will build an endpoint that returns data. Then, iterate through the code and arrive at a more expressive way to build this API endpoint. With each iteration, you should start to see how the Result pattern can be used with ease.

The Result pattern is a more structured way of handling errors, and it can be used to make your APIs more robust and easier to maintain. At the end of this take, you will see a minimal API endpoint that utilizes less code, is easier to maintain, and remains minimal.

Feel free to follow along with the code examples in this take. You can find the complete code in this GitHub repository.

The app is a simple weather forecast API that returns a list of random data. The app is built using the .NET 8.0 SDK and the Entity Framework Core in-memory database provider. The app is structured in a way that demonstrates the problem with exceptions and how this design pattern can be used to tackle error logic.

Introduction

Be sure to have the latest version of the .NET SDK installed on your machine. You can download it from the .NET website. I will be using .NET 8.0. You can check your version by running the following command in your terminal:

To fire up a new minimal API project, run the following commands:

This should be enough to get you started with a new minimal API project.

Next, add the necessary dependencies to the project. You will need the Entity Framework Core in-memory database provider. Run the following command to add the necessary dependencies:

The Entity Framework Database Context

The Data-Transfer-Objects (DTOs) are the first thing to create. These define the structure of the data that will be passed around in the application.

With any endpoint, you typically start with data then map the DTOs into the data model that will become the actual payload response. I chose Entity Framework because it runs in memory.

To set up for the endpoint we will be creating, we will create the following set of code. Start with creating a new file named WeatherDto.cs in the ResultPattern directory and add the following code:

Because I am dealing with Entity Framework Core, the DTOs must have a parameter-less constructor. This is because Entity Framework Core needs to create instances of these classes to map the database tables.

Then, create a new file named WeatherForecastContext.cs in the ResultPattern directory and add the following code:

This automatically seeds the database with some random weather forecasts for the cities in the CityDb table. There is a relationship between the city and the hourly weather forecast, which is one-to-many. Because this class gets scoped per request, I added a check to ensure that the database is only seeded once.

Next, wire the database context into the application. Open the Program.cs file in the ResultPattern directory and add the following code:

Lastly, be sure to create models that API endpoints can return. This decouples your raw tables in the database from the domain model a client can consume.

Create a new file named WeatherModel.cs in the ResultPattern directory and add the following code:

The Problem with Exceptions

Exceptions can be used to control the flow of errors in the application. This allows you to jump out of the logic and catch the error elsewhere.

To illustrate the issue, create a new file named CrummyWeatherForecastService.cs in the ResultPattern directory and add the following code:

I picked a random city like Houston to simulate an internal server error. The city code must be two characters and non-numeric. If the city code is not found, a not found exception is thrown. If the city code is not valid, an invalid exception is thrown.

Time to build the actual endpoint via Minimal APIs. If you scaffolded the project you should find everything you need in a single file like the Program.cs.

Then, in the Program.cs file, add the following code:

This calls the weather service, wraps the call around a try then attempts to catch every conceivable error that might occur in the service layer.

I am coining this try/catch hell, not only because we use exceptions to handle validation logic, but also because we must catch different types of exceptions to return the appropriate status code. The code is hard to read, because the logic jumps around and is not in a single place. Exceptions bubble up the call stack, which can make it hard to understand where the error originated.

Throwing exceptions works much like a goto statement, where the code jumps arbitrarily to a different part of the program. What’s cumbersome is how the catch must match the type of exception thrown. Exception types all inherit from the parent Exception class, and you can infinitely chain multiple errors into a parent/child hierarchy. This can lead to errors that collide with each other and end up in the wrong catch block.

To further illustrate, imagine what would happen if the service class did this instead:

Did you catch the issue? The catch block will catch all exceptions, including the not found exception. This is because the not found exception is a child of the parent exception class. This can lead to unexpected behaviour in the code because you can have deep inheritance chains when you build custom errors. At least the humble goto statement is explicit about where it jumps to. Here, a deep chain of exceptions, and a deep call stack, can make the code unpredictably hard to follow.

Throwing exceptions to handle control flow logic also has performance implications. Exceptions are expensive to create and throw, and they can slow down the application. This is because the runtime has to unwind the stack to find the appropriate catch block which can be anywhere.

Because exceptions can slow down the application, an attacker can exploit this weakness and use it as a denial-of-service attack. An attacker can flood the application with unsuccessful requests and make it unresponsive.

The Result Pattern

A better approach is to return a result object that encapsulates the success or failure of the operation. This contains either a value or an error message. A flag can be used to determine the flow of the API response.

This is the Result pattern.The pattern mostly allows you to wrap your validation results in a single place. This way it is perfectly clear what the endpoint should expect to return and handle the response. The result object now becomes a reliable source of truth for the entire application.

Create a new file named Result.cs in the ResultPattern directory and add the following code:

There are many libraries that implement this class, but you can create your own with minimal code. The Result class is a generic class that can hold any type of value. It has a Value property that holds the value if the operation is successful, and an Error property that holds the error response if the operation fails. The IsSuccess property is a flag that determines the control flow of the API response.

The Weather Forecast Service with the Result pattern

With the Result object the control flow logic can simply return this same result value. Keep in mind the purpose of the pattern is to encapsulate all return values in a single place.

Now, implement a new weather service that uses the Result pattern without throwing exceptions to tackle errors. Create a new file named WeatherService.cs in the ResultPattern directory and add the following code:

There is an implicit conversion from the WeatherModel to the Result class. This allows you to return a WeatherModel from the method, and the compiler will automatically convert it to a Result object. For error handling, the method returns a Result object with the appropriate error type. I added static methods like NotFound, Invalid, and InternalServerError to create a Result object with the appropriate error.

Any unhandled exceptions that occur are caught and logged, and an internal server error response is returned. This encapsulates the truly exceptional error handling logic in a single place, making the code easier to understand. Because I no longer rely on exceptions to handle control flow logic, the code can catch all exceptions and return an error response, which is more predictable.

Time to build our endpoint with our newfound result value. Instead of wrapping everything around a try and catching arbitrary errors, the code should be much easier to follow via control blocks.

Then, in the Program.cs file, add the following code:

The code remains somewhat the same, but the error handling is based on conditionals rather than throwing exceptions. These are value based, which makes the code easier to digest. The ErrorType enum can be extended to include more error types, such as unauthorized, forbidden, and conflict errors. This explicitly defines the type of error that occurred, without jumping around in the code.

A Bit of Functional Programming

Functional programming attempts to make the code much clearer and predictable. This Result class gives you the ability to make the code more expressive via simple ideas that you can steal from the functional paradigm.

The code has been much improved, but it can be made even better with a bit of functional concepts. Luckily, since C# 9.0, the language has been getting more features added. Features like pattern matching, and the switch expression to make the code more expressive.

A good place to start is in the Result class itself because this encapsulates the API response. The Result class is not quite a monad, because it does not have a Bind or SelectMany method for proper functional composition. But it can steal some functional concepts such as the `Match` method to make it more expressive.

Add the following code to the Result class:

This may not look like much, but it generically matches the success or error state of the Result object. Then calls the appropriate function based on the state. The delegate functions are passed in as arguments, which allows the minimal API endpoint to be more expressive.

Then, in the Program.cs file, add the following code:

There you have it. The code is more expressive and remains minimal. In minimal APIs, the handlers should be lean, sexy, and expressive, while all the complexity is put elsewhere.

The Result pattern works great with minimal APIs because it pushes all the complexity to the result class itself. In programming, you don’t really eliminate complexity, you just move it around. Luckily, once you establish this pattern, you can reuse this same technique in other endpoints without having to repeat the try/catch hell I showed you above.

A Quick Word on Exception Handling

There is an idiom that says when you have a hammer everything looks like a nail. Tackling errors with exceptions is a hammer, and it can be tempting to use it for everything. But exceptions should be used for truly exceptional cases, not for control flow logic. The runtime takes a performance hit when exceptions are thrown, and they can slow down the application.

Exceptions also behave like a goto statement on steroids, which can make the code unpredictable and hard to follow.

Conclusion

The Result pattern is a more structured way of handling errors in ASP.NET Core Minimal APIs. It can be used to make your APIs more robust and easier to maintain. The Result pattern encapsulates the success or failure of an operation in a single object, which makes the code easier to read and maintain.

Article tags

Load comments

About the author

Camilo Reyes

See Profile

Software Engineer from Houston, Texas. Passionate about C# and clean code that runs without drama. When not coding, loves to cook and work on random home projects.