Search code examples
graphqlentity-framework-corehotchocolate

Why does Resolver behave differently in Query vs Mutation?


I've been struggling with the issue/behaviour described below for while now and can't seem to figure out what's going on. This is all based on inherited/adapted code, so it's completely possible that I'm missing something fundamental. It's been literally years since I've asked a question on SO, so please bear that in mind if I'm not complying with the current expectations on formatting and content/detail.

I've got a number of domain classes involved in this problem, but at a high level, what I'm trying to do is to count the number of users with a specific role in relation to a group.

The behaviour that I'm seeing when I execute a GraphQL query (See below), is that the attribute in question (capacityUsed) is populated as expected, which makes me believe that the wiring is correct.

However, when I try to access the attribute within a GraphQL mutation, the underlying data to populate the value isn't retrieved, so the default value is returned, which isn't accurate or what I want.

Ultimately what I'm trying to do is to access (and make business decisions based on) the GroupRoleCapacity.CapacityUsed attribute. I'd like to sort out what's wrong with the config/setup for the mutation so that my resolver works as expected in both situations.

I'd appreciate any help or insight into what might be causing this. I've gone through the related HotCholocate and GraphL documentation a number of times and tried just about every iteration of related keywords that I can think of searching for an explanation of what's going on, and haven't been able to find a solution.

Here's how things are currently wired up (pulling out extraneous info/attributes to keep it short):

//GraphQL Entity Configuration
public class GroupRoleCapacityType : ObjectType<GroupRoleCapacity>
{
    protected override void Configure(IObjectTypeDescriptor<GroupRoleCapacity> descriptor)
    {
        descriptor
            .Field(grc => grc.Group)
            .ResolveWith<Resolvers>(r => r.GetGroup(default!, default!))
            .UseDbContext<AppDbContext>()
            .Description("This is the Group for this GroupRoleCapacity.");
        descriptor
            .Field(grc => grc.Role)
            .ResolveWith<Resolvers>(r => r.GetRole(default!, default!))
            .UseDbContext<AppDbContext>()
            .Description("This is the Role for this GroupRoleCapacity.");
        descriptor
            .Field(grc => grc.CapacityUsed)
            .ResolveWith<Resolvers>(r => r.GetCapacityUsed(default!, default!))
            .UseDbContext<AppDbContext>()
            .Description("This is the number of Users with the indicated Role for this GroupRoleCapacity.");
    }
    protected class Resolvers
    {
        public Group GetGroup([Parent] GroupRoleCapacity GroupRoleCapacity, [ScopedService] AppDbContext context)
        {
            return context.Groups.FirstOrDefault(p => p.Id == GroupRoleCapacity.GroupId);
        }
        public Role GetRole([Parent] GroupRoleCapacity GroupRoleCapacity, [ScopedService] AppDbContext context)
        {
            return context.Roles.FirstOrDefault(p => p.Id == GroupRoleCapacity.RoleId);
        }
        public int GetCapacityUsed([Parent] GroupRoleCapacity GroupRoleCapacity, [ScopedService] AppDbContext context)
        {
            return context.Users.Count(u => u.GroupId == GroupRoleCapacity.GroupId &&
                u.UserRoles.Any(ur => ur.RoleId == GroupRoleCapacity.RoleId));
        }
    }
}
//GraphQL Service Startup    
public class Startup
{
    public IConfiguration Configuration { get; }
    public Startup(IConfiguration configuration) { Configuration = configuration; }
    public void ConfigureServices(IServiceCollection services) {
        services.AddScoped(p => p.GetRequiredService<IDbContextFactory<AppDbContext>>().CreateDbContext());
        services
            .AddGraphQLServer()
            .AddQueryType<Query>()
            .AddMutationType<Mutation>()
            .AddType<GroupType>()
            .AddType<RoleType>()
            .AddType<UserType>()
            .AddType<UserRoleType>()
            .AddType<GroupRoleCapacityType>()
    }
}

Using the query below seems to work as expected and the GroupRoleCapacity.CapacityUsed attribute is populated appropriately when accessing the data via a query

{ groupRoleCapacity { groupId roleId capacityUsed }}

//GraphQL Query
public class Query
{
    [UseDbContext(typeof(AppDbContext))]
    [GraphQLType(typeof(NonNullType<ListType<NonNullType<GroupRoleCapacities.GroupRoleCapacityType>>>))]
    public IQueryable<GroupRoleCapacity> GetGroupRoleCapacity([ScopedService] AppDbContext context)
    {
        return context.GroupRoleCapacities;
    }
}

However, when I use a mutation similar to below, GroupRoleCapacity.CapacityUsed isn't being set correctly, which is what's making me suspect a problem with either the Resolver implementation or the configuration.

mutation { addUserToGroup(input: { userId: 1, groupId: 1, roleId: 1}) { result { userId, groupId, roleId }}}

//GraphQL Mutation 
public class Mutation
{
    public Mutation(IConfiguration configuration) { }
    
    [UseDbContext(typeof(AppDbContext))]
    public async Task<AddUserToGroupPayload> AddUserToGroupAsync(AddUserToGroupInput input,
        [ScopedService] AppDbContext context
        )
    {
        int someLimit = 5;
        var roleCapacity = context.GroupRoleCapacities.First(grc =>
            grc.GroupId == input.GroupId &&
            grc.RoleId == input.RoleId);
        if(roleCapacity.CapacityUsed > someLimit) {
            throw ApplicationException("limit exceeded");
        }
        //add user to group and save etc

        return new AddUserToGroupPayload(result);
    }
}

Solution

  • Difference here that Hot Chocolate is used only for GraphQL querying, but your code calls EF Core context explicitly. You need some common part.

    What you can do:

    1. Better expose DTO instead of database entity
    2. Write common query which returns this DTO
    3. Feed Hot Chocolate with this query. In this case you do not need resolvers at all (if I understand this library correctly)

    Sample query (without DTO, never used Hot Chocolate and not familiar with it's attributes)

    public static class MyQueries
    {
        public static IQueryable<GroupRoleCapacity> GetGroupRoleCapacity(AppDbContext context)
        {
            return context.GroupRoleCapacities.Select(c => new GroupRoleCapacity
            {
                GroupId = c.GroupId,
                Group = c.Group, // I assume you have defined navigation properties
                Role  = c.Role,
                RoleId = c.RoleId,
                CapacityUsed = await context.Users.Count(u => u.GroupId == c.GroupId &&
                    u.UserRoles.Any(ur => ur.RoleId == c.RoleId))
            );
        }
    }
    

    Query:

    //GraphQL Query
    public class Query
    {
        [UseDbContext(typeof(AppDbContext))]
        [GraphQLType(typeof(NonNullType<ListType<NonNullType<GroupRoleCapacities.GroupRoleCapacityType>>>))]
        public IQueryable<GroupRoleCapacity> GetGroupRoleCapacity([ScopedService] AppDbContext context)
        {
            return MyQueries.GetGroupRoleCapacity(context);
        }
    }
    

    Mutation:

    //GraphQL Mutation 
    public class Mutation
    {
        public Mutation(IConfiguration configuration) { }
        
        [UseDbContext(typeof(AppDbContext))]
        public async Task<AddUserToGroupPayload> AddUserToGroupAsync(AddUserToGroupInput input,
            [ScopedService] AppDbContext context
            )
        {
            int someLimit = 5;
            var roleCapacity = MyQueries.GetGroupRoleCapacity(context)
                .FirstAsync(grc =>
                    grc.GroupId == input.GroupId &&
                    grc.RoleId == input.RoleId);
    
            if (roleCapacity.CapacityUsed > someLimit) 
            {
                throw ApplicationException("limit exceeded");
            }
            //add user to group and save etc
    
            return new AddUserToGroupPayload(result);
        }
    }