In our journey with GraphQL, sooner or later, we are going to need to pick a strategy to handle errors during mutations. There is a great post "A Guide to GraphQL Errors" where the author shows several options (its pros and cons). Based on this, we will implement the last stage presented there: Stage 6a: Error Union List + Interface.
We will modify the code presented here, please clone or download it. Open the solution and add a file named IError.cs
with the following content:
public interface IError
{
string Message { get; set; }
}
public class NotEmptyError : IError
{
public string Message { get; set; } = null!;
public NotEmptyError(string field)
{
Message= $"The {field} is required";
}
}
public class MaxLengthError : IError
{
public string Message { get; set; } = null!;
public MaxLengthError(string field, int maxLength)
{
Message = $"The max length for {field} is {maxLength}";
}
}
Open the PostMutations.cs
file, and modify the AddPostPayload
record as follow:
public record AddPostPayload(Post? post, IError[]? errors);
In the same file, modify the AddPost
method with the following content:
public AddPostPayload AddPost(AddPostInput input, [Service] Storage storage)
{
var errors = new List<IError>();
if(string.IsNullOrEmpty(input.Body))
{
errors.Add(new NotEmptyError(nameof(AddPostInput.Body)));
}
if (string.IsNullOrEmpty(input.Title))
{
errors.Add(new NotEmptyError(nameof(AddPostInput.Title)));
}
if (input.Body?.Length >= 256)
{
errors.Add(new MaxLengthError(nameof(AddPostInput.Body), 256));
}
if (input.Title?.Length >= 1024)
{
errors.Add(new MaxLengthError(nameof(AddPostInput.Title), 1024));
}
if (errors.Any())
{
return new AddPostPayload(null, errors.ToArray());
}
var post = new Post() { Id = Guid.NewGuid(), Body = input.Body, Title = input.Title };
storage.Posts.Add(post);
return new AddPostPayload(post, null);
}
All the code to support the errors is done, time to setup the schema to support it. Start creating a file named ErrorType.cs
. Here we are going to register an interface, two errors and an union:
public class ErrorType : InterfaceType<IError>
{
protected override void Configure(
IInterfaceTypeDescriptor<IError> descriptor)
{
}
}
public class MaxLengthErrorType : ObjectType<MaxLengthError>
{
protected override void Configure(
IObjectTypeDescriptor<MaxLengthError> descriptor)
{
descriptor.Implements<ErrorType>();
}
}
public class NotEmptyErrorType : ObjectType<NotEmptyError>
{
protected override void Configure(
IObjectTypeDescriptor<NotEmptyError> descriptor)
{
descriptor.Implements<ErrorType>();
}
}
public class AddPostPayloadErrorType : UnionType
{
protected override void Configure(IUnionTypeDescriptor descriptor)
{
descriptor.Name("AddPostPayloadError");
descriptor.Type<MaxLengthErrorType>();
descriptor.Type<NotEmptyErrorType>();
}
}
Create a file named AddPostPayloadType.cs
with the following content:
public class AddPostPayloadType : ObjectType<AddPostPayload>
{
protected override void Configure(IObjectTypeDescriptor<AddPostPayload> descriptor)
{
descriptor
.Field(f => f.post)
.Type<PostType>();
descriptor
.Field(f=>f.errors)
.Type<ListType<AddPostPayloadErrorType>>();
}
}
And finally, update the PostMutationsType.cs
file as follows:
public class PostMutationsType : ObjectType<PostMutations>
{
protected override void Configure(
IObjectTypeDescriptor<PostMutations> descriptor)
{
descriptor.Field(f => f.AddPost(default!, default!)).Type<AddPostPayloadType>();
descriptor.Field(f => f.RemovePost(default!, default!));
descriptor.Field(f => f.EditPost(default!, default!));
}
}
Run the solution, go to https://localhost:7121/graphql/
and run the following mutation:
mutation {
addPost(input: { body: "", title: "" }) {
post {
id
}
errors {
... on MaxLengthError {
message
}
... on NotEmptyError {
message
}
}
}
}
To see a result such as:
{
"data": {
"addPost": {
"post": null,
"errors": [
{
"message": "The Body is required"
},
{
"message": "The Title is required"
}
]
}
}
}
We did it! but there is a lot of boilerplate code. Luckily, HotChocolate has an implementation of this pattern out of the box with a difference, the errors are going to be thrown as exceptions. Create a new file NotFoundException.cs
as follows:
public class NotFoundException : Exception
{
public NotFoundException(Guid id) : base($"The with {id} was not found")
{
}
}
Open the PostMutations.cs
file and modify the EditPost
method with the following content:
public Post EditPost(EditPostInput input, [Service] Storage storage)
{
var post = storage.Posts.FirstOrDefault(post => post.Id == input.Id);
if(post==null)
{
throw new NotFoundException(input.Id);
}
post.Body = input.Body;
return post;
}
Go to the PostMutationsType.cs
file and update the code as follow:
public class PostMutationsType : ObjectType<PostMutations>
{
protected override void Configure(
IObjectTypeDescriptor<PostMutations> descriptor)
{
descriptor.Field(f => f.AddPost(default!, default!)).Type<AddPostPayloadType>();
descriptor.Field(f => f.RemovePost(default!, default!));
descriptor.Field(f => f.EditPost(default!, default!))
.UseMutationConvention()
.Error<NotFoundException>();
}
}
Finally, go to the Program.cs
file and enable the mutation conventions with AddMutationConventions
:
builder.Services
.AddGraphQLServer()
.AddMutationConventions(applyToAllMutations: false)
.AddQueryType<PostQueriesType>()
.AddMutationType<PostMutationsType>()
.AddFiltering()
.AddSorting();
Run the solution and run the following mutation:
mutation {
editPost(input: { body: "abc", id: "5e629e6fb8d64126196c7d12cb6f8a6a" }) {
post {
id
}
errors {
... on NotFoundError {
message
}
}
}
}
To get the same result as our manual implementation:
{
"data": {
"editPost": {
"post": null,
"errors": [
{
"message": "The with 5e629e6f-b8d6-4126-196c-7d12cb6f8a6a was not found"
}
]
}
}
}
You can check more about this feature here. The final code of this post is available here. Thanks, and happy coding.