ChilliCream / graphql-platform

Welcome to the home of the Hot Chocolate GraphQL server for .NET, the Strawberry Shake GraphQL client for .NET and Banana Cake Pop the awesome Monaco based GraphQL IDE.
https://chillicream.com
MIT License
5.23k stars 744 forks source link

Returning errors from DataLoaders #6796

Closed tobias-tengler closed 2 months ago

tobias-tengler commented 9 months ago

Product

Hot Chocolate

Is your feature request related to a problem?

There's a difference between not finding an entity by it's key and being unable to, due to unexpected circumstances. To handle partial errors inside a DataLoader you currently have two options:

Both of these options are not perfect from a DX perspective.

The solution you'd like

It would be great if there were some helpers for dealing with partial DataLoader results. I've seen there already is a Result helper with Reject and Resolve methods in Green Donut, but it doesn't seem to be used anywhere. Ideally the dataloader.LoadAsync(id) could detect whether the returned item is an error or not and re-throw the exception.

michaelstaib commented 2 months ago

This is supported since day 1 :)

michaelstaib commented 2 months ago

The base DataLoader fetch looks like the following:

    protected override ValueTask FetchAsync(
        IReadOnlyList<TKey> keys,
        Memory<Result<TValue>> results,
        CancellationToken cancellationToken)

Result:

/// <summary>
/// A wrapper for a single value which could contain a valid value or any
/// error.
/// </summary>
/// <typeparam name="TValue">A value type.</typeparam>
public readonly record struct Result<TValue>
{
    /// <summary>
    /// Creates a new value result.
    /// </summary>
    /// <param name="value">The value.</param>
    public Result(TValue value) : this()
    {
        Error = default;
        Value = value;
        Kind = ResultKind.Value;
    }

    /// <summary>
    /// Creates a new error result.
    /// </summary>
    /// <param name="error">
    /// The error.
    /// </param>
    public Result(Exception error)
    {
        Error = error ?? throw new ArgumentNullException(nameof(error));
        Value = default!;
        Kind = ResultKind.Error;
    }

    /// <summary>
    /// Gets a value indicating whether the result is an error, a value or undefined.
    /// </summary>
    public ResultKind Kind { get; }

    /// <summary>
    /// Gets the value. If <see cref="Kind"/> is <see cref="ResultKind.Error"/>, returns
    /// <c>null</c> or <c>default</c> depending on its type.
    /// </summary>
    public TValue Value { get; }

    /// <summary>
    /// Gets an error If <see cref="Kind"/> is <see cref="ResultKind.Error"/>;
    /// otherwise <c>null</c>.
    /// </summary>
    public Exception? Error { get; }

    /// <summary>
    /// Creates a new error result.
    /// </summary>
    /// <param name="error">An arbitrary error.</param>
    /// <returns>An error result.</returns>
    public static Result<TValue> Reject(Exception error) => new(error);

    /// <summary>
    /// Creates a new value result.
    /// </summary>
    /// <param name="value">An arbitrary value.</param>
    /// <returns>A value result.</returns>
    public static Result<TValue> Resolve(TValue value) => new(value);

    /// <summary>
    /// Creates a new error result or a null result.
    /// </summary>
    /// <param name="error">An arbitrary error.</param>
    public static implicit operator Result<TValue>(Exception? error)
        => error is null ? new Result<TValue>(default(TValue)!) : new Result<TValue>(error);

    /// <summary>
    /// Creates a new value result.
    /// </summary>
    /// <param name="value">An arbitrary value.</param>
    public static implicit operator Result<TValue>(TValue value)
        => new(value);

    /// <summary>
    /// Extracts the error from a result.
    /// </summary>
    /// <param name="result">An arbitrary result.</param>
    public static implicit operator Exception?(Result<TValue> result)
        => result.Error;

    /// <summary>
    /// Extracts the value from a result.
    /// </summary>
    /// <param name="result">An arbitrary result.</param>
    public static implicit operator TValue(Result<TValue> result)
        => result.Value;
}

However, we removed result from the higher level abstractions because most use-cases do not need it ... but you can use it when directly inheriting from DataLoader<TK, TV>.