Tor Haugen
Tor Haugen

Reputation: 19627

Exposing many-to-many relations using GraphQL.EntityFramework

I have a fairly straight forward .NET Core EF model, backed by a SQL Server database, which I am working to expose via GraphQL in a .NET Core Web API using GraphQL.EntityFramework (which in turn builds on GraphQL for .NET).

I have a few many-to-many relations such as this one:

Many-to-many relation between stores and categories

The entity model has this form (irrelevant bits removed for clarity):

public class StoreEntity
{
    [Key]
    public int StoreId { get; set; }
    public string Name { get; set; }

    // etc...

    public List<StoreCategoryEntity> Categories { get; set; }
}

public class StoreCategoryEntity
{
    [Required]
    public int StoreId { get; set; }
    [Required]
    public int CategoryId { get; set; }

    public CategoryEntity Category { get; set; }
}

public class CategoryEntity
{
    [Key]
    public int CategoryId { get; set; }
    public string Name { get; set; }
}

...because that's how EF likes it (and it makes sense).

Now, I would like to expose this as GraphQL on this form:

query {
  store (id:1) {
    storeId
    name
    categories {
      categoryId
      name
    }
  }
}

Using GraphQL.EntityFramework, the typical object graph type would be something like this (this is the constructor):

public StoreType(IEfGraphQLService<MyDbContext> efGraphQlService) : base(efGraphQlService)
{
    Field(x => x.StoreId).Description("The store ID");
    Field(x => x.Name).Description("The store name");

    // ...

    AddNavigationListField(
        name: "categories",
        resolve: context => context.Source.Categories);
}

Now, this requires an object graph type for StoreCategory, and the graph shape would be like this:

query {
  store (id:1) {
    storeId
    name
    categories {
      storeId
      categoryId
      category {
        categoryId
        name
      }
    }
  }
}

This works fine, but is somewhat more convoluted than I would like it. I would like the navigation list field to be of type Category rather than StoreCategory. I have tried this:

public StoreType(IEfGraphQLService<MyDbContext> efGraphQlService) : base(efGraphQlService)
{
    Field(x => x.StoreId).Description("The store ID");
    Field(x => x.Name).Description("The store name");

    AddNavigationListField(
        name: "categories",
        resolve: context => context.Source.Categories.Select(x => x.Category));
}

...but the categories returned are all null. It seems GraphQL.EntityFramework, which tries to be intelligent about EF "Includes" (and mostly succeeds), doesn't pick up on the need to include Category.

Can anyone with experience using GraphQL.EntityFramework give me a pointer here?

Edit:

I have managed to make it work like this:

Field<ListGraphType<CategoryType>>(
    name: "categories",
    resolve: context =>
    {
        var dbContext = ResolveDbContext(context);
        return dbContext.StoreCategories
            .AsNoTracking()
            .Include(x => x.Category)
            .Where(x => x.StoreId == context.Source.StoreId)
            .Select(x => x.Category);
    });

...but this code will make a separate DB roundtrip for each store, which is excactly the kind of thing GraphQL.EntityFramework is designed to avoid.

Upvotes: 0

Views: 710

Answers (1)

Tor Haugen
Tor Haugen

Reputation: 19627

This works:

public StoreType(IEfGraphQLService<MyDbContext> efGraphQlService) : base(efGraphQlService)
{
    Field(x => x.StoreId).Description("The store ID");
    Field(x => x.Name).Description("The store name");

    // ...

    AddNavigationListField(
        name: "categories",
        resolve: context => context.Source.Categories.Select(x => x.Category),
        includeNames: new[] { "Categories.Category" })
        .Description = "The categories for the store";

}

The trick is to use the dot format for includes, which lets you define an include "path".

The reason this is necessary is that while GraphQL.EntityFramework will apply all includes in the includeNames argument array, they are all applied at the source entity level (here: Store) - there is no direct support for .ThenInclude().

"Categories.Category" is equivalent to .Include(x => x.Categories).ThenInclude(x => x.Category).

Upvotes: 1

Related Questions