GraphQL with .NET Core (Part - VIII: 1-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.
Building a GraphQL
end-point with a single entity ain't gonna cut it. In this post, we introduce two new entities for handling orders for a customer. The relationship between Customer
and Order
is one-to-many i.e. A customer can have one or many orders, whereas a particular order belongs to a single customer.
You can configure entity relationship following entity framework conventions. Entity framework will auto-create a one-to-many relationship between entities if one of the entity contains a collection property of the second entity. This property is known as a navigation property.
In Customer
entity, you have Orders
as a collection navigation property.
public class Customer
{
public int CustomerId { get; set; }
public string Name { get; set; }
public string BillingAddress { get; set; }
public ICollection<Order> Orders { get; set; }
}
Most of the time, a navigation property of collection type is enough to declare a one-to-many relationship. However, it is suggested that you declare a fully defined relationship. To achieve that, on the second entity you define a foreign key property along with a reference navigation property
Following represents the Order
entity where CustomerId
is a foreign key and the Customer
is a reference navigation property.
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; }
}
A property is considered a navigation property if the type it points to can not be mapped as a scalar type by the current database provider.
- docs.microsoft.com
I've added two new ObjectGraphType
s for defining accessible fields of Order
and Customer
as followings,
public class OrderType : ObjectGraphType<Order>
{
public OrderType(IRepository repository)
{
Field(o => o.Tag);
Field(o => o.CreatedAt);
FieldAsync<CustomerType, Customer>("customer",
resolve: ctx =>
{
return repository.GetCustomerById(ctx.Source.CustomerId);
});
}
}
public class CustomerType : ObjectGraphType<Customer>
{
public CustomerType(IRepository repository)
{
Field(c => c.Name);
Field(c => c.BillingAddress);
FieldAsync<ListGraphType<OrderType>, IReadOnlyCollection<Order>>(
"items",
resolve: ctx => {
return repository.GetOrdersByCustomerId(ctx.Source.CustomerId);
});
}
}
To expose two new end-points for accessing all the customers and orders, I've registered two new fields of ListGraphType
inside the GameStoreQuery
as following,
FieldAsync<ListGraphType<OrderType>, IReadOnlyCollection<Order>>(
"orders",
resolve: ctx =>
{
return repository.GetOrders();
});
FieldAsync<ListGraphType<CustomerType>, IReadOnlyCollection<Customer>>(
"customers",
resolve: ctx =>
{
return repository.GetCustomers();
});
Implementations of the newly added fetch methods inside Repository.cs
are as following,
public async Task<IReadOnlyCollection<Order>> GetOrders()
{
return await _applicationDbContext.Orders.AsNoTracking().ToListAsync();
}
public async Task<IReadOnlyCollection<Customer>> GetCustomers()
{
return await _applicationDbContext.Customers.AsNoTracking().ToListAsync();
}
public async Task<Customer> GetCustomerById(int customerId)
{
return await _applicationDbContext.Customers.FindAsync(customerId);
}
public async Task<IReadOnlyCollection<Order>> GetOrdersByCustomerId(int customerId)
{
return await _applicationDbContext.Orders.Where(o => o.CustomerId == customerId).ToListAsync();
}
I've also threw in two additional methods for creating Customer
and Order
,
public async Task<Order> AddOrder(Order order)
{
var addedOrder = await _applicationDbContext.Orders.AddAsync(order);
await _applicationDbContext.SaveChangesAsync();
return addedOrder.Entity;
}
public async Task<Customer> AddCustomer(Customer customer)
{
var addedCustomer = await _applicationDbContext.Customers.AddAsync(customer);
await _applicationDbContext.SaveChangesAsync();
return addedCustomer.Entity;
}
As you already guessed, we have two new DbSet<>
in our ApplicationDbContext
class,
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
{
}
public DbSet<Item> Items { get; set; }
public DbSet<Order> Orders { get; set; }
public DbSet<Customer> Customers { get; set; }
}
Of course, you have to add these method signatures in your repository interface as well for abstraction and DI,
public interface IRepository
{
/* Code removed for brevity */
Task<IReadOnlyCollection<Order>> GetOrders();
Task<IReadOnlyCollection<Customer>> GetCustomers();
Task<Customer> GetCustomerById(int customerId);
Task<IReadOnlyCollection<Order>> GetOrdersByCustomerId(int customerId);
Task<Order> AddOrder(Order order);
Task<Customer> AddCustomer(Customer customer);
}
Remember the last post on mutation, you had to create a new InputObjectGraphType
for Item
in order to create side effects. Likewise, the followings are the InputObjectGraphType
for Customer
and Order
.
public class OrderInputType : InputObjectGraphType
{
public OrderInputType()
{
Name = "OrderInput";
Field<NonNullGraphType<StringGraphType>>("tag");
Field<NonNullGraphType<DateGraphType>>("createdAt");
Field<NonNullGraphType<IntGraphType>>("customerId");
}
}
public class CustomerInputType : InputObjectGraphType
{
public CustomerInputType()
{
Name = "CustomerInput";
Field<NonNullGraphType<StringGraphType>>("name");
Field<NonNullGraphType<StringGraphType>>("billingAddress");
}
}
To expose two new end-points for creating customer and order, I've added two new fields inside the GameStoreMutation
as following,
public class GameStoreMutation : ObjectGraphType
{
public GameStoreMutation(IRepository repository)
{
/* Code removed for brevity */
FieldAsync<CustomerType>(
"createCustomer",
arguments: new QueryArguments(
new QueryArgument<NonNullGraphType<CustomerInputType>> { Name = "customer" }
),
resolve: async context =>
{
var customer = context.GetArgument<Customer>("customer");
return await repository.AddCustomer(customer);
});
FieldAsync<OrderType>(
"createOrder",
arguments: new QueryArguments(
new QueryArgument<NonNullGraphType<OrderInputType>> { Name = "order" }
),
resolve: async context =>
{
var order = context.GetArgument<Order>("order");
return await repository.AddOrder(order);
});
}
}
Finally, we need to register all the types with the DI system. Newly created services registration inside ConfigureServices
are as followings,
public void ConfigureServices(IServiceCollection services)
{
/* Code removed for brevity */
services.AddTransient<CustomerType>();
services.AddTransient<CustomerInput>();
services.AddTransient<OrderType>();
services.AddTransient<OrderInputType>();
/* Code removed for brevity */
}
Now, run the application and make sure you can access the newly added fields,