graphql-platform icon indicating copy to clipboard operation
graphql-platform copied to clipboard

Default sort or required order parameter

Open eliottrobson opened this issue 3 years ago • 4 comments

Is your feature request related to a problem?

The order parameter for queries when using .UseSorting is optional. When running queries with .Paging the query generated looks similar to:

SELECT `x`.`Id`
FROM `MyObject` AS `x`
LIMIT @__p_0

In EntityFramework this logs the warning:

The query uses a row limiting operator ('Skip'/'Take') without an 'OrderBy' operator. This may lead to unpredictable results.

The solution you'd like

Is there any mechanism to setup a default sort inside the SortInputType?

I noticed there was a DefaultValue method but I couldn't find any documentation about what is supposed to go here. If this is not desired, would you consider allowing us to make the parameter not optional?

class MyObjectSortType : SortInputType<MyObject>
{
    protected override void Configure(ISortInputTypeDescriptor<MyObject> descriptor)
    {
        descriptor.BindFieldsExplicitly();
        descriptor.Field(f => f.Id);
         // Can this be used to set a default sort?
         // .DefaultValue(Utf8GraphQLParser.Syntax.ParseValueLiteral("ASC"));

        // Proposed fluent interface
        descriptor.DefaultValue(x => x.Id, OrderType.ASC);
    }
}

Product

Hot Chocolate

eliottrobson avatar Aug 26 '21 19:08 eliottrobson

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

stale[bot] avatar May 04 '22 11:05 stale[bot]

Still relevant, commenting to un-stale the issue.

hognevevle avatar May 04 '22 11:05 hognevevle

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

stale[bot] avatar Jul 03 '22 12:07 stale[bot]

Still relevant.

hognevevle avatar Jul 03 '22 18:07 hognevevle

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

stale[bot] avatar Nov 01 '22 19:11 stale[bot]

Actually this is already solved in 12 and more elegantly in 13

michaelstaib avatar Nov 01 '22 21:11 michaelstaib

Actually this is already solved in 12 and more elegantly in 13

@michaelstaib Do you have examples? 🙂

glen-84 avatar Nov 03 '22 16:11 glen-84

Yes ... this is for version 12

[UsePaging]
[UseFiltering(typeof(AssetFilterInputType))]
[UseSorting(typeof(AssetSortInputType))]
public IQueryable<Asset> GetAssets(AssetContext context, IResolverContext resolverContext)
    => resolverContext.ArgumentLiteral<IValueNode>("order").Kind is SyntaxKind.NullValue
        ? context.Assets.OrderBy(t => t.Price!.TradableMarketCapRank)
        : context.Assets;

michaelstaib avatar Nov 18 '22 19:11 michaelstaib

@michaelstaib I'm using 13, is it different?

glen-84 avatar Nov 18 '22 19:11 glen-84

You can use the same.

michaelstaib avatar Nov 18 '22 19:11 michaelstaib

You can use the same.

Would you mind elaborating a bit more on the v13 part? You mentioned above that it was solved more elegantly in 13, so what does that mean exactly? :)

hognevevle avatar Jan 26 '23 18:01 hognevevle

@michaelstaib

Would you consider reopening this to look at terser solutions as proposed by the OP?

Defining this in the SortInputType seems like a cleaner option, and avoids having to repeatedly look at the order argument in resolvers.

glen-84 avatar May 18 '23 11:05 glen-84

We decided on a different direction, automatically applying a sort by ID to ensure deterministic ordering.

/// <summary>
/// Extends the <see cref="UseSortingAttribute" /> to apply an additional order by ID, to ensure
/// deterministic ordering.
/// </summary>
public sealed class UseDeterministicSortingAttribute<T> : UseSortingAttribute
{
    public UseDeterministicSortingAttribute([CallerLineNumber] int order = 0)
        : base(typeof(T), order) { }

    protected override void OnConfigure(
        IDescriptorContext context,
        IObjectFieldDescriptor descriptor,
        MemberInfo member)
    {
        descriptor.Use(next => async middlewareContext =>
        {
            // Execute the regular sorting middleware.
            await next(middlewareContext);

            if (middlewareContext.Result is IQueryable queryable)
            {
                // Only apply deterministic sorting when the element type is assignable to IEntity.
                if (!queryable.ElementType.IsAssignableTo(typeof(IEntity)))
                {
                    return;
                }

                // Get the name of the ordering method to invoke, depending on whether or not
                // ordering has already been applied.
                var methodName = queryable.Expression.Type.IsAssignableTo(typeof(IOrderedQueryable))
                    ? nameof(Queryable.ThenBy)
                    : nameof(Queryable.OrderBy);

                // Find the appropriate method to invoke.
                var method = typeof(Queryable)
                    .GetMethods()
                    .Single(m => m.Name == methodName && m.GetParameters().Length == 2)
                        .MakeGenericMethod(queryable.ElementType, typeof(long));

                // Create the lambda expression for ordering (e => e.Id).
                var parameter = Expression.Parameter(queryable.ElementType, "e");
                var body = Expression.Property(parameter, nameof(IEntity.Id));
                var keySelector = Expression.Lambda(body, parameter);

                // Invoke the ordering method on the queryable.
                middlewareContext.Result = method.Invoke(
                    queryable,
                    new object[] { queryable, keySelector });
            }
        });

        base.OnConfigure(context, descriptor, member);
    }
}

There is also this option if you prefer to set a specific default order.

glen-84 avatar May 22 '23 13:05 glen-84