GraphQL with .NET Core (Part - IX: Many-Many Entity Relations)
Code samples used in this blog series have been updated to latest version of .NET Core (5.0.4) and GraphQL-Dotnet (4.2.0). Follow this link to get the updated samples.
In a real-world scenario, only a limited quantity of a particular item belongs to a particular order. Imagine, you have an order cart containing references to some selected items with their respective ordered quantities. The end result is you got a relationship something like: an order can have many items whereas an item can be part of multiple orders.
By EF Core conventions, in a many-to-many relation, you have two standalone entities and a third entity between them which represents a relationship bridge. In our case, the bridging entity will be the OrderItem
public class OrderItem
{
public int Id { get; set; }
public int ItemId { get; set; }
public Item Item { get; set; }
public int Quantity { get; set; }
public int OrderId { get; set; }
public Order Order { get; set; }
}
Here we have two reference navigation properties for each side of the relation i.e. Order
and Item
. Also, we have two foreign key properties i.e. ItemId
and OrderId
.
For a fully defined relationship, we also have individual collection navigation property of OrderItem
on each side of the standalone entities,
public class Order
{
public int OrderId { get; set; }
public string Tag { get; set; }
public DateTime CreatedAt { get; set; }
public Customer Customer { get; set; }
public int CustomerId { get; set; }
public IEnumerable<OrderItem> OrderItems { get; set; }
}
public class Item
{
public int Id { get; set; }
public string Tag { get; set; }
public string Title { get; set; }
public decimal Price { get; set; }
public IEnumerable<OrderItem> OrderItems { get; set; }
}
We are using an In-Memory database so no migration script is needed for each iterative changes on the database. But, once you have configured all the necessary relationships, create a migration and update your development/production database using dotnet CLI with the following commands,
dotnet ef migrations add ManyToManyRelationship
dotnet ef database update
We can add a GraphQL
end-point for adding an item to a particular order. To do that we need an InputGraphTypeType
for OrderItem
.
public class OrderItemInputType : InputObjectGraphType
{
public OrderItemInputType()
{
Name = "OrderItemInput";
Field<NonNullGraphType<IntGraphType>>("quantity");
Field<NonNullGraphType<IntGraphType>>("itemId");
Field<NonNullGraphType<IntGraphType>>("orderId");
}
}
As for the end-point, we registered a new mutation field inside GameStoreMutation.cs
. The field is simply named addOrderItem
,
FieldAsync<OrderItemType>(
"addOrderItem",
arguments: new QueryArguments(
new QueryArgument<NonNullGraphType<OrderItemInputType>> { Name = "orderItem" }
),
resolve: async ctx =>
{
var orderItem = ctx.GetArgument<OrderItem>("orderItem");
return await repository.AddOrderItem(orderItem);
});
Newly added OrderItemType
is as following,
public class OrderItemType : ObjectGraphType<OrderItem>
{
public OrderItemType(IRepository repository)
{
Field(i => i.ItemId);
FieldAsync<ItemType, Item>("item", resolve: ctx =>
{
return repository.GetItemById(ctx.Source.ItemId);
});
Field(i => i.Quantity);
Field(i => i.OrderId);
FieldAsync<OrderType, Order>("order", resolve: ctx =>
{
return repository.GetOrderById(ctx.Source.OrderId);
});
}
}
Newly registered methods from Repository.cs
are as following,
public async Task<Item> GetItemById(int itemId)
{
return await _applicationDbContext.Items.FindAsync(itemId);
}
public async Task<Order> GetOrderById(int orderId)
{
return await _applicationDbContext.Orders.FindAsync(orderId);
}
public async Task<OrderItem> AddOrderItem(OrderItem orderItem)
{
var addedOrderItem = await _applicationDbContext.OrderItem.AddAsync(orderItem);
await _applicationDbContext.SaveChangesAsync();
return addedOrderItem.Entity;
}
I've also threw in an additional field for querying a list of all the OrderItem
at once,
FieldAsync<ListGraphType<OrderItemType>, IReadOnlyCollection<OrderItem>>(
"orderItem",
resolve: ctx =>
{
return repository.GetOrderItem();
});
Repository code for GetOrderItem
is as following,
public async Task<IReadOnlyCollection<OrderItem>> GetOrderItem()
{
return await _applicationDbContext.OrderItem.AsNoTracking().ToListAsync();
}
Last but not least, don't forget to register the newly added graph types with the DI system. Services registration inside ConfigureServices
are as followings,
services.AddTransient<OrderItemType>();
services.AddTransient<OrderItemInputType>();
That's all about it. Run the application and try to add an item to a particular order with a mutation like the following illustration,
Repository Link
Important Links
Configuring Many To Many Relationships in Entity Framework Core