⚠️ Clear And Actionable Error Messages In .Net
Achieve robust error handling in .Net APIs by implementing best practices with the Generic class and lambda expressions
Introduction
Handling errors in web APIs can be a tedious task. But with the right approach, it can be made simple and effective. In this blog post, we’ll discuss the importance of providing clear and specific error messages in web APIs and the best practices for achieving that. We’ll take a close look at the generic exception class and how it can be used to handle errors in a consistent and clear way.
Problem 🤔
The problem we are trying to address in this blog post is the lack of consistency and specificity in error handling in web APIs. Traditional error handling methods often lead to generic and unhelpful error messages that do not provide enough information for the user or developer to understand and resolve the issue. This can lead to frustration for the user and can also make it difficult for developers to identify and fix the problem.
Lets take the CreateAccount
action method. The method has a simple validation logic and returns a BadRequest
result if the person
object does not pass the validation. Error should allow the client to receive clear and specific error messages that can be used to identify and resolve the issue.
[HttpPost]
public IActionResult CreateAccount([FromBody] Person person)
{
// 🔬 Validation Logic
// ⚠️ Return Error If Person Is Invalid
// 👌 Continue...
}
[HttpPost]
public IActionResult CreateAccount([FromBody] Person person)
{
// 🔬 Validation Logic
// ⚠️ Return Error If Person Is Invalid
// 👌 Continue...
}
Person can be a simple class or record with three properties Name
, Address
and Mobile
.
public class Person
{
public string Name { get; set; }
public string Address { get; set; }
public string Mobile { get; set; }
};
public class Person
{
public string Name { get; set; }
public string Address { get; set; }
public string Mobile { get; set; }
};
public record Person(string Name, string Address, string Mobile);
public record Person(string Name, string Address, string Mobile);
Solution/TLDR 🎯
We can create generic class InvalidInput<TSource>
that can be used to return errors for any type. The Add
method takes a lambda expression that selects a property of the source type, and a string message.
InvalidInput
This code defines a generic InvalidInput<TSource>
class that can be used to store a collection of invalid properties and their associated error messages, it has a dictionary to map error messages with respective properties, and Add
method to add properties to the invalid properties dictionary and uses lambda expression to get the name of the property.
public class InvalidInput<TSource>
{
// Map error messages with respective property
public Dictionary<string, string> InvalidProperties { get; } = new();
// Add method for adding properties to the invalid properties dictionary
public InvalidInput<TSource> Add<TProp>(
Expression<Func<TSource, TProp>> keySelector,
string message
)
{
// Use the lambda expression to get the name of the property
var propertyName = ((MemberExpression)keySelector.Body).Member.Name;
InvalidProperties[propertyName] = message;
return this;
}
}
public class InvalidInput<TSource>
{
// Map error messages with respective property
public Dictionary<string, string> InvalidProperties { get; } = new();
// Add method for adding properties to the invalid properties dictionary
public InvalidInput<TSource> Add<TProp>(
Expression<Func<TSource, TProp>> keySelector,
string message
)
{
// Use the lambda expression to get the name of the property
var propertyName = ((MemberExpression)keySelector.Body).Member.Name;
InvalidProperties[propertyName] = message;
return this;
}
}
Usage
The CreateAccount
method creates an instance of the InvalidInput<Person>
class. In this case, the lambda expressions are used to select the Address
and Mobile
properties of the Person
class, and assigning the error message to it.
[HttpPost]
public IActionResult CreateAccount([FromBody] Person person)
{
// 🏗️ Build Error Message
var error = new InvalidInput<Person>()
.Add(p => p.Address,
"Services are currently unavailable in your area")
.Add(p => p.Mobile,
"Mobile is already registered");
// ⚠️ Return Error If Person Is Invalid
return BadRequest(error.InvalidProperties);
// 👌 Continue...
}
[HttpPost]
public IActionResult CreateAccount([FromBody] Person person)
{
// 🏗️ Build Error Message
var error = new InvalidInput<Person>()
.Add(p => p.Address,
"Services are currently unavailable in your area")
.Add(p => p.Mobile,
"Mobile is already registered");
// ⚠️ Return Error If Person Is Invalid
return BadRequest(error.InvalidProperties);
// 👌 Continue...
}
Output
The output of this method would be a BadRequest
HTTP response with the error object as the Response body
.
{
"Address": "Services are currently unavailable in your area",
"Mobile": "Mobile is already registered"
}
{
"Address": "Services are currently unavailable in your area",
"Mobile": "Mobile is already registered"
}
Properties > Generic Message
Returning properties in the error instead of just a generic message provides several benefits:
Specificity
When a user receives an error message, they expect it to be as specific as possible. By returning the properties that are invalid, the user can quickly identify which inputs are causing the error and take action to correct them.
Contextual information
When returning properties in the error, the user can see the context of the error in relation to the input data. This can help the user understand why the error occurred and how to fix it.
Debugging
When debugging, developers can use the properties in the error to identify the source of the problem more easily. They can also use this information to trace the error through the codebase.
Localization
By returning properties in the error, developers can create more specific error messages that can be localized for different languages. This improves the user experience for non-english speakers and helps them to understand the error message.
Automation
When the error message contains the specific properties that caused the error, it’s possible to automate the process of correcting the error by using the information to pre-populate the input fields.
Lambda > String
Using lambda expressions to get the name of a property provides several benefits:
Readability
Using a lambda expression to select a property is more readable than hardcoding the property name as a string. It makes the code more self-explanatory, and it’s clear to see which property is being selected.
Type safety
Since the lambda expression is strongly typed, it can catch errors at compile-time if the property name is mistyped or if the property does not exist on the type. This can prevent runtime errors and make the code more robust.
Refactor-friendly
If the property name is refactored (e.g. renamed), the lambda expression will automatically update to reflect the new name. This can save a lot of time and effort when making changes to the codebase.
Reusability
The InvalidInput<TSource>
class is generic and can be used for any type, as long as the property names are selected using lambda expressions. This makes the class more versatile and reusable.