A set of extensions for working with HotChocolate GraphQL and Database access with micro-orms such as RepoDb (or Dapper). This extension pack provides access to key elements such as Selections/Projections, Sort arguments, & Paging arguments in a significantly simplified facade so this logic can be leveraged in the Serivces/Repositories that encapsulate all data access (without dependency on IQueryable and execution outside of the devs control).
(Unofficial) HotChocolate v11/v12 Extension Pack for working with Micro-ORM(s) and encapsulated data access (instead of IQueryable).
This library greatly simplifies working with HotChocolate to resolver processes data for selecting/projecting, sorting, paging, etc. before returning the data to HotChocolate from the resolver; this is in contrast to the often documented deferred execution via IQueryable whereby control over the execution is delegated to the HotChocolate (& EntityFramework) internals.
By providing a facade that enables easier selections, projections, and paging inside the resolver this library greatly simplifies working with HotChocolate GraphQL and Database access with micro-orms such as RepoDb (or Dapper). This extension pack provides access to key elements such as Selections/Projections, Sort arguments, & Paging arguments in a significantly simplified facade so this logic can be leveraged inside the Resolvers (and lower level Serivces/Repositories that encapsulate all data access) without dependency on IQueryable deferred execution (e.g. EntityFramework).
In addition, for RepoDb specifically, a full implementation is provided along with additional RepoDb extension methods for efficient/easy Cursor Pagination and is built completely on top of the graphql.ResolverProcessingExtensions.
I'm happy to share with the community, but if you find this useful (e.g for professional use), and are so inclinded, then I do love-me-some-coffee!
A set of extensions for working with HotChocolate GraphQL and Database access with micro-orms such as RepoDb (or Dapper). Micro ORMs normally require (and encourage) encapsulated data access that resolver processes the results prior to returning results from the resolvers to HotChocolate.
This is in contrast to how many existing tutorials illustrate deferred execution of database queries via IQueryable. Because IQueryable is pretty much only supported by Entity Framework, it may be incongruent with existing tech-stacks and/or be much to large (bloated) of a dependency -- in addition to removing control over the SQL queries.
In these cases, and in other cases where existing repository/service layer code already exists, the data is 'resolver processed', and is already filtered, sorted, paginated, etc. before being returned from the graphql resolvers.
This extension pack provides access to key elements such as Selections/Projections, Sort arguments, & Paging arguments in a significantly simplified facade so this logic can be leveraged in the Serivces/Repositories that encapsulate all data access (without dependency on IQueryable and execution outside of the devs control).
To use this in your project, add the graphql.PreprocessingExtensions NuGet package to your project, wire up your Starup middleware, and inject / instantiate params in your resolvers as outlined below...
A set of extensions for working with HotChocolate GraphQL and RepoDb as the data access micro-orm without dependency on IQueryable. This enables fully encapsulated control over SQL queries in every way within a Service or Repository layer of your application.
This extension pack provides a significantly simplified facade to access critial elements such as Selections/Projections, Sort arguments, & Paging arguments with support for mapping them to Models using built in RepoDb functionality. It also leverages RepoDb to provide a generic, Relay spec compatible, cursor pagination/slice query api for Sql Server.
To use this in your project, add the graphql.RepDb.SqlServer NuGet package to your project, wire up your Starup middleware, and inject / instantiate params in your resolvers as outlined below...
graphqlParamsContext.IsTotalCountRequested
This project provides multiple versions of the HotChocolate GraphQL Star Wars example project using the Pure Code First approach.
Each of the examples are setup to run as an AzureFunctions app and updated/enhanced to use the new v11 API along with example cases
for the various features of this package -- CharacterQueries and CharacterRepository now all use RepoDb and ResolverProcessing extensions
to push logic down to be encapsulated in the data access layer.
Two versions of the Example Project included:
TODO... add implementation summary
NOTE: The HotChocolate default behaviour will occur anytime a normal IEnumerable or IQueryable result is returned. This is accomplished by ensuring that the new Sorting/Paging Providers have "right of first refusal" for handling, but will always default back to the existing HotChocolate Queryable implementations.
builder.Services
.AddgraphqlServer()
.AddQueryType<YourQueryResolverClass>()
.SetPagingOptions(new PagingOptions()
{
DefaultPageSize = 10,
IncludeTotalCount = true,
MaxPageSize = 100
})
//This Below is the initializer to be added...
//NOTE: This Adds Sorting & Paging providers/conventions by default! Do not AddPaging() &
// AddSorting() in addition to '.AddResolverProcessedResultsExtensions()', or the HotChocolate
// Pipeline will not work as expected!
.AddResolverProcessedResultsExtensions()
[UsePaging]
[UseSorting]
[graphqlName("characters")]
public async Task<IResolverProcessedCursorSlice<ICharacter>> GetCharactersPaginatedAsync(
[Service] ICharacterRepository repository,
//This facade is now injected by the Resolver Processing extensions middleware...
[graphqlParams] IParamsContext graphqlParams
)
{
//Sample Extraced from StarWars-AzureFunctions-RepoDb example project:
namespace StarWars.Characters
{
[ExtendObjectType(Name = "Query")]
public class CharacterQueries
{
[UsePaging]
[UseSorting]
[graphqlName("characters")]
public async Task<IResolverProcessedCursorSlice<ICharacter>> GetCharactersPaginatedAsync(
//This is just our Repository being injected as a normal Service configured in Startup.cs
[Service] ICharacterRepository repository,
//This facade is now injected by the Resolver Processing extensions middleware...
[graphqlParams] IParamsContext graphqlParams
)
{
//With HotChocolate.RepoDb.SqlServer package we can easily map the inputs from graphql
// IParamsContext into the RepoDb specific helper for DB mapping:
// NOTE: Other Micro ORMs (e.g. Dapper) may need similar mapping capabiliteis to be built
// and are not yet provided here in this project...
var repoDbParams = new graphqlRepoDbMapper<CharacterDbModel>(graphqlParams);
//********************************************************************************//
// Push the Selections, SortArgumetns, and Paging Arguments down to our Repository
// layer as RepoDb specific mapped models (Field, OrderField, etc.
// Using the exentions for Cursor Pagination our Repository API reuturns
// a 'Page Slice' model.
var charactersSlice = await repository.GetPagedCharactersAsync(
repoDbParams.SelectFields,
repoDbParams.SortOrderFields ?? OrderField.Parse(new { name = Order.Ascending })
repoDbParams.PagingParameters
).ConfigureAwait(false);
//Now With a valid Page/Slice we can return a ResolverProcessed Cursor Result so that
// it will not have any additional post-processing in the HotChocolate pipeline!
//NOTE: Filtering can be applied but it will ONLY be applied to the results we
// are now returning because this would normally be pushed down to the
// Sql Database layer also (pending support).
return charactersSlice.AsResolverProcessedCursorSlice();
//*******************************************************************************//
}
}
}
public class QueryResolverHelpers
{
public SomeResult DoSomethingWithTheResovlerContext(IResolverContext resolverContext)
{
var paramsContext = new graphqlParamsContext(resolverContext);
...... now you can work with selections, sort args, paging arguments (including IsTotalCountRequested) etc. easily.....
{
paramsContext.GetSelectFields()
. //Here we define an extension to the Human GraphQL type and expose a 'droids' field via our virtual resolver.
[ExtendObjectType(nameof(Human))]
public class HumanFieldResolvers
{
//However, we MUST have the Id field of the parent entity `Character.Id` as part of the original
// selection in order to get related droid data! This is done by defining a dependency here
// with the [ResolverProcessingDependencies(....)] attribute; we state that this resolver is dependent
// on the Character entity's Id field!
//NOTE: The original resolver for the parent Character entities will know about this dependency
// automatically (auto-magically) because the `Id` field will be included in the selection fields,
// anytime the 'droids' field is requested, due to this dependency, and will be readily available
// field when the parent resolver calls paramsContext.GetSelectFields().
//NOTE: You can specify any number of dependencys (as string names) in the one attribute via the params aray.
[graphqlName("droids")]
[ResolverProcessingParentDependencies(nameof(ICharacter.Id))]
public async Task<IEnumerable<Droid>> GetDroidsAsync(
[Service] ICharacterRepository repository,
[Parent] ICharacter character
)
{
//NOW we can rely on the fact that Character.Id won't be null because the parent Resolver
// had it as a field to be selected and populated.
//NOTE: Error checking isn't a bad idea anyway...
var friends = await repository.GetCharacterFriendsAsync(character.Id).ConfigureAwait(false);
var droids = friends.OfType<Droid>();
return droids;
}
}
public class HumanType : ObjectType<Human>
{
protected override void Configure(IObjectTypeDescriptor<Human> descriptor)
{
descriptor.Name("human");
descriptor.Field(t => t.Name).Type<NonNullType<StringType>>();
descriptor.Field(t => t.Friends).Name("friends")
//Manually define a Selection/Projection dependency on the "Id" field of
// the parent entity so that it is always provided to the parent resolver.
//This helps ensure that the value is not null in our resolver anytime "friends"
// is part of the selection, and the parent "Id" field is not.
.AddResolverProcessingParentProjectionDependencies(nameof(Human.Id))
.Resolver(ctx =>
{
var repository = ctx.Service<IRepository>();
var parentHumanId = ctx.Parent<Human>().Id;
return repository.GetRelatedHumans(parentHumanId);
});
}
}