This content originally appeared on DEV Community and was authored by Marcin Janiak
This article is about the strategy of handling errors in your C# GraphQL APIs and is based on Sasha Solomon's article about handling errors in GraphQL environments.
It's adopted to asp.net core server implementation (Hot Chocolate) and it's a glimpse of how it might look and be used in your next GraphQL application.
TLDR;
Mostly, the mentioned article is about separating "errors" into categories:
- errors (e.g. Internal Server Error)
- results (Deleted User, Unavailable in Country)
Later on, it encourages to use union types for response modeling. It also highlights the pros of this approach and shows the cons of using the default approach.
Default approach
GraphQL errors mostly look like this one.
{
"data": {
"user": null
},
"errors": [
{ "path": [ "user" ],
"locations": [ { "line": 2, "column": 3 } ],
"extensions": {
"message": "Object not found",
"type": 2
}
}
]
}
That generates multiple problems:
- All errors are treated the same
- It’s hard to know where the error came from
- It’s hard for the client to know what errors to care about
I'd like to show you one extra problem that comes to my mind:
Due to app evolution and development of new business rules - some set of operations might return new kinds of errors which might be unknown until they occur...on production!
Really, go ahead and read the mentioned article, it's cool!
Before we start
We will be creating a simple wallet app responsible for withdrawals, deposits and retrieving historical operations.
To keep things simple, let's use an in-memory repository (registered as a singleton). Keep in mind that persistence isn't a subject of this post (and of a git repository)!
We will be using .NET 6 framework and a minimal API approach.
For a GraphQL server, we will be using Hot Chocolate (12.3.1 version).
If you don't know Hot Chocolate framework, feel free to read about it on chillicream.com (official website) and feel free to join the great community (which is always willing to help) on slack
Moreover, the guys from Chilli Cream are really passionate about GraphQL - delivering the whole ecosystem to create and interact with your GraphQL solutions.
Hot Chocolate allows us to create APIs in three ways:
- Annotation based
- Code-First
- Schema-First
I will focus on Code-First, but it is possible to adapt the code to the other ones.
First things first!
The wallet app is a pretty simple case - we cannot withdraw an amount greater than our current balance and we cannot operate on a blocked wallet.
Also, we might be able to perform these:
- Get a wallet
- Get a total balance
- Get a history of a operations
- Deposit
- Withdraw
In this post, we will focus on modelling the data and implementing withdrawals and deposits.
Model the data
Let's start with modeling the financial operation itself:
public enum OperationType
{
Deposit,
Withdrawal
}
public record FinancialOperation(decimal Amount, OperationType Type, DateTimeOffset At);
Continue with Wallet class
public class Wallet
{
public Guid Id { get; }
public ICollection<FinancialOperation> Operations { get; }
public Wallet(Guid id, ICollection<FinancialOperation> operations)
{
Id = id;
Operations = operations;
}
}
Implement both Deposit and Withdraw methods:
public decimal GetBalance()
{
return Operations.Sum(x => x.Type == OperationType.Deposit ? x.Amount : -x.Amount);
}
public void Withdraw(decimal amount)
{
if (amount > GetBalance())
{
throw new InsufficientBalanceException(amount);
}
Operations.Add(new FinancialOperation(amount,
OperationType.Withdrawal,
DateTimeOffset.UtcNow));
}
public void Deposit(decimal amount)
{
Operations.Add(new FinancialOperation(amount,
OperationType.Deposit,
DateTimeOffset.UtcNow));
}
We want to throw a custom exception if the balance is insufficient to perform a withdrawal:
public class InsufficientBalanceException : Exception
{
public decimal RequestedAmount { get; }
public InsufficientBalanceException(decimal requestedAmount) : base(
$"Insufficient balance to withdraw {requestedAmount}")
{
RequestedAmount = requestedAmount;
}
}
Modeling the mutation
public class Mutation
{
public Guid OpenNewWallet([FromServices] IWalletRepository walletRepository)
{
var id = Guid.NewGuid();
walletRepository.Add(new Wallet(id,
new List<FinancialOperation>()));
return id;
}
public bool Deposit(Guid id, decimal amount, [FromServices] IWalletRepository walletRepository)
{
var wallet = walletRepository.Get(id);
wallet.Deposit(amount);
return true;
}
public bool Withdraw(Guid id, decimal amount, [FromServices] IWalletRepository walletRepository)
{
var wallet = walletRepository.Get(id);
wallet.Withdraw(amount);
return true;
}
}
With the current implementation, we are able to:
- Get NullException if the wallet wasn't created because we didn't do any null handling
- Get our custom Exception if a wallet was created, but no money was deposited.
- Withdraw successfully (and get true result)
We could improve it with exception handling - and return false for failed request - but we can do it even better.
Improving withdrawal with union response (the kind of a good way)
Let's model out the response the way it should look (at least in GraphQL ecosystem):
public record WithdrawalResult(Guid WalletId, decimal WithdrawnAmount);
public record WalletNotFound(Guid WalletId);
public record InsufficientFunds(Guid WalletId, decimal Amount);
The ObjectType (via Code First method in Hot Chocolate)
public class WithdrawResultType : UnionType
{
protected override void Configure(IUnionTypeDescriptor descriptor)
{
base.Configure(descriptor);
descriptor.Type<ObjectType<WithdrawalResult>>();
descriptor.Type<ObjectType<WalletNotFound>>();
descriptor.Type<ObjectType<InsufficientFunds>>();
}
}
Don't forget to use new ObjectType!
public class MutationType : ObjectType<Mutation>
{
protected override void Configure(IObjectTypeDescriptor<Mutation> descriptor)
{
base.Configure(descriptor);
descriptor.Field(x => x.Withdraw(default,
default,
default!))
.Type<WithdrawResultType>();
}
}
The two biggest drawbacks of this approach are:
- we can return anything and get a new kind of error.
- we don't know what the allowed types are
Pushing it further to embrace the dotnet typings
Due to the missing of union types in dotnet (they might be available in C# 11) - we have to improvise.
Let's have an empty interface IWithdrawalResult so we can prevent the wrong types from being returned.
public IWithdrawalResult Withdraw(Guid id, decimal amount, [FromServices] IWalletRepository walletRepository)
{/* the body of Withdraw method stays the same */}
public interface IWithdrawalResult {}
public record WithdrawalResult(Guid WalletId, decimal WithdrawnAmount) : IWithdrawalResult;
public record WalletNotFound(Guid WalletId) : IWithdrawalResult;
public record InsufficientFunds(Guid WalletId, decimal Amount) : IWithdrawalResult;
public class WithdrawResultType : UnionType<IWithdrawalResult> {/* the body of Configure method stays the same */}
It's a little better, but...let's refactor the deposit method using the same approach.
Refactoring the whole API as fast as possible
The shown approach seems to be right and we want to refactor the whole API to use this instead. The fastest possible way might be reusing the types.
We are going to try achieving the following with the reuse of WalletNotFound result.
So, we:
- Create IDepositResult interface
- Create DepositResult and make it implement mentioned interface
- Make WalletNotFound implement IDepositResult
- Create DepositResultType and configure this union type
- Refactor Deposit method
- Configure Deposit field in MutationType
public interface IDepositResult {}
public record DepositResult(Guid WalletId, decimal DepositedAmount) : IDepositResult;
public record WalletNotFound(Guid WalletId) : IWithdrawalResult, IDepositResult;
/* Part of Mutation class */
public IDepositResult Deposit(Guid id, decimal amount, [FromServices] IWalletRepository walletRepository)
{
var wallet = walletRepository.Get(id);
if (wallet is null)
{
return new WalletNotFound(id);
}
wallet.Deposit(amount);
return new DepositResult(id,
amount);
}
public class DepositResultType : UnionType<IDepositResult>
{
protected override void Configure(IUnionTypeDescriptor descriptor)
{
base.Configure(descriptor);
descriptor.Type<ObjectType<DepositResult>>();
descriptor.Type<ObjectType<WalletNotFound>>();
}
}
public class MutationType : ObjectType<Mutation>
{
protected override void Configure(IObjectTypeDescriptor<Mutation> descriptor)
{
base.Configure(descriptor);
descriptor.Field(x => x.Withdraw(default,
default,
default!))
.Type<WithdrawResultType>();
descriptor.Field(x => x.Deposit(default,
default,
default!))
.Type<DepositResultType>();
}
}
Hurray! It works! We can freely reuse types and have cool schemas...
Not really! I really hope at this point you all know what's wrong.
Imagine if any of the result interfaces would have some properties
public interface IDepositResult
{
public string DepositRelatedProperty { get; }
}
public interface IWithdrawalResult
{
public string WithdrawalRelatedProperty { get; }
}
public record WalletNotFound(Guid WalletId, string DepositRelatedProperty, string WithdrawalRelatedProperty)
: IWithdrawalResult, IDepositResult;
Every time we want to return the WalletNotFound result we have to be aware of multiple properties coming from various sources. Also, we might end up having types implementing an enormous number of interfaces.
The usability of this approach also depends on code organization in your project. Probably - if the results are scattered - you won't be having a good time searching for all of them.
Doing it the good way
Please note: This approach multiplies the amount of code you have to write but it feels much cleaner. Also - we are totally abandoning the concept of reusing the types.
From now, we have only to do two things.
Refactoring the result types (and fixing corresponding ones)
with a help of nested classes (records in this case).
Keep in mind that now we can change names to be less expressive.
public interface IDepositResult
{
public record Valid(Guid WalletId, decimal DepositedAmount) : IDepositResult;
public record NotFound(Guid WalletId) : IDepositResult;
}
public interface IWithdrawalResult
{
public record Valid(Guid WalletId, decimal WithdrawnAmount) : IWithdrawalResult;
public record NotFound(Guid WalletId) : IWithdrawalResult;
public record InsufficientFunds(Guid WalletId, decimal Amount) : IWithdrawalResult;
}
Avoiding conflicts in the naming of types
Due to how GraphQL works (and Hot Chocolate) - we cannot have multiple types with the same name (eg. subclass Valid in IDepositResult and IWithdrawResult)
We can easily fix it by implementing our own DefaultNamingConventions.
The example implementation might look like this:
public class CustomUnionTypesNamingConventions : DefaultNamingConventions
{
public override NameString GetTypeName(Type type)
{
return type.DeclaringType is null ? type.Name : $"{type.Name}{RemoveIFromDeclaringInterfaceName(type)}";
}
public override NameString GetTypeName(Type type, TypeKind kind)
{
var name = type.DeclaringType is null ? type.Name : $"{type.Name}{RemoveIFromDeclaringInterfaceName(type)}";
if (kind == TypeKind.Union)
{
return new NameString(name.Substring(1) + "Union");
}
return new NameString(name);
}
private string RemoveIFromDeclaringInterfaceName(Type type)
{
if (type.DeclaringType is not null && type.DeclaringType.IsInterface)
{
return type.DeclaringType.Name.Substring(1);
}
return type.DeclaringType is null ? string.Empty : type.DeclaringType.Name;
}
}
The pros:
- We can easily expand and modify each result type without the risk of breaking something else
- The compiler watches over the types we return
The cons:
- Frontend cannot use fragments
- We have a lot more types
- We cannot easily modify multiple results in API
Summary
Each of shown methods might be sufficient for your needs, currently, I'm using two of them (the interface one and the subclass one) in two different projects.
It feels like the last one is the most comfortable for me.
The interface approach is most appropriate in terms of creating GraphQL API.
On the second hand, the subclasses approach is better from the C# developer standpoint.
Please keep in mind that it might be used for queries too.
The links (once again)
chillicream.com (official website)
The full code
marcin-janiak
/
200ok-hotchocolate-error-handling
Dotnet implementation of 200OK Error Handling in Hot Chocolate.
This content originally appeared on DEV Community and was authored by Marcin Janiak
Marcin Janiak | Sciencx (2021-11-22T19:30:32+00:00) Adopting 200 OK! Error Handling strategy in GraphQL with Hot Chocolate (dotnet).. Retrieved from https://www.scien.cx/2021/11/22/adopting-200-ok-error-handling-strategy-in-graphql-with-hot-chocolate-dotnet/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.




