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; }
}
Customer.cs

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; }
}
Order.cs
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 ObjectGraphTypes 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);
            });
    }
}
OrderType.cs
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);
            });
    }
}
CustomerType.cs

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();
    });
GameStoreQuery.cs

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();
}
Repository.cs

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;
}
Repository.cs

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);
}
IRepository.cs

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");
	}
}
OrderInputType.cs
public class CustomerInputType : InputObjectGraphType
{
	public CustomerInputType()
	{
		Name = "CustomerInput";
		Field<NonNullGraphType<StringGraphType>>("name");
		Field<NonNullGraphType<StringGraphType>>("billingAddress");
	}
}
CustomerInputType.cs

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);
           });
    }
}
GameStoreMutation.cs

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,

Adding a customer

Adding an order
Getting an order and customer

Part-VIII

fiyazbinhasan/GraphQLCoreFromScratch
https://fiyazhasan.me/tag/graphql/. Contribute to fiyazbinhasan/GraphQLCoreFromScratch development by creating an account on GitHub.

Modeling EF Core Relationships

Tracking vs No-Tracking Queries