Simple Graph API solution template using Dapper ORM for .NET 8 with clean code, clean architecture and CQRS in mind.. Ideal if you need to set up an Graph API on a existing database. Just install and use the template and start adding queries and mutations.
If you have used this template, learned something or like what you see, consider giving it a star!
Install GraphR
dotnet new --install GraphR
Scaffold your solution
Create a folder, for example 'MyNewSolution' and run this command inside the folder:
dotnet new graphr -n MyNewSolution
The complete solution will be scaffolded inside your folder. Open it in Visual Studio:
Replace the connectionString
in appsettings.json
with your own and remove the Seed
folder in the Infrastructure
project.
Remove the example queries/mutation/repositories etc and implement your own.
In the core layer we have a custom lightweight handler implementation. These are Handler<TOutput>
for querying data without parameters,
and Handler<TInput, TOutput>
for querying or mutating data with parameters.
Handler is a abstract class so we can leverage the validation method leveraging FluentValidation.
You still need to define a interface for dependency injection, the handlers are automatically registered as scoped.
Since we don't use AutoMapper
to map input types to output types, we just write an extension method ToOutput()
that maps TInput
to ToOutput()
.
FluentValidation
is used to validate input parameter properties.
internal sealed class GetBookByIdQueryHandler : Handler<GetBookByIdParameters, BookDto>, IGetBookByIdHandler
{
private readonly IBookRepository _bookRepository;
public GetBookByIdQueryHandler(IBookRepository bookRepository)
{
_bookRepository = bookRepository;
}
protected override void DefineRules()
{
RuleFor(x => x.Id).GreaterThan(0);
}
protected override async Task<BookDto> HandleValidatedRequest(GetBookByIdParameters request)
=> (await _bookRepository.GetById(request.Id)).ToOutput();
}
public interface IGetBookByIdHandler : IHandler<GetBookByIdParameters, BookDto> { }
Inject the IGetBookByIdQueryHandler
in your GraphApi query using the HotChocolate [Service]
attribute, do the same for Mutations.
public sealed class BooksQuery
{
public async Task<BookDto> Book([Service] IGetBookByIdHandler handler, GetBookByIdParameters parameters)
=> await handler.Handle(parameters);
}
This is an example of how your GraphApi application layer could be structured
Currently supports a DbConnectionProvider
for a single SQL database connection. Inject the IDbConnectionProvider
in your repositories.
In the domain folder there is a abstraction of the DbTransaction
implementation. You can inject the ITransaction
interface in a MutationHandler
in your Application layer if you need to mutate data over multiple repositories.
internal class ExampleMutationHandler : Handler<ExampleMutationParameters, Result>,
IExampleMutationHandler
{
private readonly IRepository _repository;
private readonly ISecondRepository _secondRepository;
private readonly ITransaction _transaction;
public ExampleMutationHandler(
IRepository secondRepository,
ISecondRepository secondRepository,
ITransaction transaction)
{
_repository = repository;
_secondRepository = secondRepository;
_transaction = transaction;
}
protected override void DefineRules()
{
// FluentValidation RuleSet for your input
}
protected override async Task<Result> HandleValidatedRequest(ExampleMutationParameters input)
{
_transaction.Begin()
try
{
await _repository.Create(...);
await _secondRepository.UpdateSomething( ... );
_transaction.Commit();
}
catch
{
_transaction.RollBack();
throw;
}
return new Result(...);
}
}
public interface IExampleMutationHandler : IHandler<ExampleMutationParameters, Result> { }
A .NET 8 WebAPI application, here we have our GraphAPI endpoint. This application depends on the Application layer, and it also has a reference to the Infrastructure layer to wire up the IoC container (dependency injection), so we only use it in the ServiceCollectionExtensions
.
This layer contains all application logic and it depends only on the Domain and Core layer. Here we implement the handlers, graphApi input and output types, queries and mutations.
This will contain all models, enums, exceptions, interfaces, types and logic specific to the domain layer. This project references no other project.
In this layer we implement the data access (repositories, Dapper implementation, EntityMaps..) and possibly classes to access other external resources. These classes should be based on interfaces defined within the application layer.
This project holds some core logic that we need in our solution. In the template, we have logic that wires up the custom handler system and the method to register Dapper IEntityMap<T>
registrations.
This project uses the Bindicate
library to autowire dependencies by using attributes, so we don't bloat the ServiceCollectionExtensions
.
Build and run the project, and navigate to /graphql, this will take you to the Banana Cake Pop playground where you can find the graphql schema:
You can play around in the Banana Cake Pop playground and test out a query.
For example: