Skip to content

Instantly share code, notes, and snippets.

@benmccallum
Last active June 19, 2023 02:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save benmccallum/89d4d5b604d67094418956db43386ce5 to your computer and use it in GitHub Desktop.
Save benmccallum/89d4d5b604d67094418956db43386ce5 to your computer and use it in GitHub Desktop.
HotChocolate Node extensions for exposing dbId

The following files demonstrate a couple of ways to hook up a dbId field across all your Node implementing types.

The first approach is to use the custom [NodeEx] (instead of [Node]) on annotation-based types and the .AsNodeEx(...) extension method for fluent-style configuration.

The second approach is to use [Node] as usual, and the standard ext methods (ImplementsNode, IdField, ResolveNode) from HC, but add a type interceptor to find node-implementing types and try determine the id field's backing member by convention, adding the dbId field from there.

IMO, on v12 or lower at least, the first approach (although it requires you to use non-framework stuff and remember that, and is a bit more code) is the way to go for two reasons.

  1. Because we're being given information about the id field's member (rather than trying to determine it from convention), there's no chance they could fall out of sync (e.g. doing [Node(IdField = "CompletlyRandomjbsadfbasdf")] won't cause the type interceptor to error.
  2. (nit) It adds the dbId field before/next to your node's id field, rather than at the end as the type interceptor does. So it's a bit cleaner.

That said, I've since learned (or believe anyway) that the Node attribute stores some context data about things it determines are node-related, like which member is for the id field. So if we can register our type interceptor after that, we could use that context knowledge to remove the issue 1 mentioned.

Or I just contrib to HC core and make dbId a first-class citizen.

using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using HotChocolate.Configuration;
using HotChocolate.Types.Descriptors;
using HotChocolate.Types.Descriptors.Definitions;
namespace HotChocolate.Types.Relay
{
/// <summary>
/// A type interceptor that adds a dbId field to Node implementing types
/// by convention.
/// </summary>
public class AddDbIdFieldToNodeTypesTypeInterceptor : TypeInterceptor
{
public override void OnBeforeCompleteType(
ITypeCompletionContext completionContext,
DefinitionBase? definition,
IDictionary<string, object?> contextData)
{
if (completionContext.Type is ObjectType objectType &&
definition is ObjectTypeDefinition objTypeDef &&
objTypeDef.RuntimeType is not null &&
objTypeDef.Interfaces
.Any(i => i is ExtendedTypeReference r && r.Type.Type.Name == "NodeType"))
{
var runtimeType = objTypeDef.RuntimeType;
var candidateIdMemberNames = new List<string>()
{
"Id",
$"{objTypeDef.Name}Id",
$"{objTypeDef.Name}ID"
};
if (objTypeDef.Name != runtimeType.Name)
{
candidateIdMemberNames.Add($"{runtimeType.Name}Id");
candidateIdMemberNames.Add($"{runtimeType.Name}ID");
}
var conventionName = completionContext.DescriptorContext.Naming.GetTypeName(runtimeType);
if (conventionName != objTypeDef.Name &&
conventionName != runtimeType.Name)
{
candidateIdMemberNames.Add($"{conventionName}Id");
candidateIdMemberNames.Add($"{conventionName}ID");
}
MemberInfo? idFieldMember = null;
foreach (var candidateIdMemberName in candidateIdMemberNames)
{
idFieldMember = runtimeType.GetMember(candidateIdMemberName).SingleOrDefault();
if (idFieldMember is not null)
{
break;
}
}
if (idFieldMember is null)
{
completionContext.ReportError(
new SchemaErrorBuilder()
.SetMessage(
$"Could not find id field member for type '{objTypeDef.Name}' " +
$"with runtime type '{runtimeType.Name}'." +
$"The following member names were tried: {string.Join(", ", candidateIdMemberNames)}")
.Build());
return;
}
var type = completionContext.TypeInspector.GetReturnTypeRef(idFieldMember);
var field = new ObjectFieldDefinition(
FieldNames.DbId,
"Identifies the primary key from the database",
type)
{
SourceType = objTypeDef.RuntimeType,
ResolverType = objTypeDef.RuntimeType,
ResolverMember = idFieldMember
};
objTypeDef.Fields.Add(field);
}
}
}
}
#nullable enable
using System;
using System.Linq;
using System.Reflection;
using HotChocolate.Types.Descriptors;
using HotChocolate.Utilities;
namespace HotChocolate.Types.Relay
{
/// <summary>
/// An implementation of the <see cref="NodeAttribute"/> that also
/// sets up our dbId field.
/// </summary>
public class NodeExAttribute : NodeAttribute
{
public override void OnConfigure(IDescriptorContext context, IObjectTypeDescriptor descriptor, Type type)
{
// Call base, which sets up normal stuff
base.OnConfigure(context, descriptor, type);
// Then following how the base does it to find the member to get the id
MemberInfo? idFieldMember = null;
if (IdField is not null)
{
idFieldMember = type.GetMember(IdField).FirstOrDefault();
}
else if (context.TypeInspector.GetNodeIdMember(type) is { } foundIdFieldMember)
{
idFieldMember = foundIdFieldMember;
}
if (idFieldMember is null)
{
throw new NotImplementedException();
}
var returnType = idFieldMember.GetReturnType();
var dbIdField = descriptor.DbIdField().ResolveWith(idFieldMember).Type(returnType);
// Just like in AsNodeEx there seems to be an issue with auto-typing with a string
if (dbIdField != null && returnType == typeof(string))
{
dbIdField.Type<NonNullType<StringType>>();
}
}
}
}
#nullable enable
using System;
using System.Linq.Expressions;
using HotChocolate.Types.Relay;
namespace HotChocolate.Types
{
public static class NodeObjectTypeExtensions
{
/// <summary>
/// Custom wrapper around built-in Relay node support.
/// Additionally adds field for `dbId`.
/// </summary>
/// <param name="descriptor"></param>
/// <param name="idProperty">Expression to the property to use for the id.</param>
/// <param name="nodeResolver">Resolver implementation.</param>
/// <returns>The field descriptor for chaining.</returns>
public static IObjectFieldDescriptor AsNodeEx<TNode, TId>(this IObjectTypeDescriptor<TNode> descriptor,
Expression<Func<TNode, TId>> idProperty,
NodeResolverDelegate<TNode?, TId> nodeResolver)
where TNode : class//?
{
// Add dbId which should just return the internal id as is
var dbIdFieldDescriptor = descriptor.DbIdField().ResolveWith(Box(idProperty));
// TODO: This is a bit dodgy but not sure how else to force it. Some seem to wanna be String not String!
if (typeof(TId) == typeof(string))
{
dbIdFieldDescriptor.Type<NonNullType<StringType>>();
}
// Call standard stuff
#pragma warning disable CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
// Suppressed until it's fixed in HC: https://github.com/ChilliCream/hotchocolate/issues/2015
return descriptor.ImplementsNode().IdField(idProperty).ResolveNode(nodeResolver);
#pragma warning restore CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
}
public static IObjectFieldDescriptor DbIdField<T>(this IObjectTypeDescriptor<T> descriptor)
=> descriptor.Field(FieldNames.DbId);
public static IObjectFieldDescriptor DbIdField(this IObjectTypeDescriptor descriptor)
=> descriptor.Field(FieldNames.DbId);
public static IInterfaceFieldDescriptor DbIdField<T>(this IInterfaceTypeDescriptor<T> descriptor)
=> descriptor.Field(FieldNames.DbId);
public static IInterfaceFieldDescriptor DbIdField(this IInterfaceTypeDescriptor descriptor)
=> descriptor.Field(FieldNames.DbId);
private static Expression<Func<TInput, object?>> Box<TInput, TOutput>(Expression<Func<TInput, TOutput>> expression)
{
Expression converted = Expression.Convert(expression.Body, typeof(object));
return Expression.Lambda<Func<TInput, object?>>(converted, expression.Parameters);
}
}
}
#nullable enable
using System;
using System.Linq;
using System.Reflection;
using HotChocolate.Types.Descriptors;
using HotChocolate.Utilities;
namespace HotChocolate.Types.Relay
{
/// <summary>
/// An implementation of the <see cref="NodeAttribute"/> that also
/// sets up our dbId field.
/// </summary>
public class NodeExAttribute : NodeAttribute
{
public override void OnConfigure(IDescriptorContext context, IObjectTypeDescriptor descriptor, Type type)
{
// Call base, which sets up normal stuff
base.OnConfigure(context, descriptor, type);
// Then following how the base does it to find the member to get the id
MemberInfo? idFieldMember = null;
if (IdField is not null)
{
idFieldMember = type.GetMember(IdField).FirstOrDefault();
}
else if (context.TypeInspector.GetNodeIdMember(type) is { } foundIdFieldMember)
{
idFieldMember = foundIdFieldMember;
}
if (idFieldMember is null)
{
throw new NotImplementedException();
}
var returnType = idFieldMember.GetReturnType();
var dbIdField = descriptor.DbIdField().ResolveWith(idFieldMember).Type(returnType);
// Temporary fix for resolver compiler bug (Michael will probably fix in 12.5.1 or 12.6.0)
dbIdField.Extend().OnBeforeCompletion((ctx, obj) =>
{
obj.SourceType = obj.ResolverType;
});
// Just like in AsNodeEx there seems to be an issue with auto-typing with a string
if (dbIdField != null && returnType == typeof(string))
{
dbIdField.Type<NonNullType<StringType>>();
}
}
}
}
#nullable enable
using System;
using System.Linq.Expressions;
using HotChocolate.Types.Relay;
namespace HotChocolate.Types
{
public static class NodeObjectTypeExtensions
{
/// <summary>
/// Custom wrapper around built-in Relay node support.
/// Additionally adds field for `dbId`.
/// </summary>
/// <param name="descriptor"></param>
/// <param name="idProperty">Expression to the property to use for the id.</param>
/// <param name="nodeResolver">Resolver implementation.</param>
/// <returns>The field descriptor for chaining.</returns>
public static IObjectFieldDescriptor AsNodeEx<TNode, TId>(
this IObjectTypeDescriptor<TNode> descriptor,
Expression<Func<TNode, TId>> idProperty,
NodeResolverDelegate<TNode, TId> nodeResolver)
where TNode : class
{
// Add dbId which should just return the internal id as is
var idPropertyFunc = idProperty.Compile();
var dbIdFieldDescriptor = descriptor
.DbIdField()
.Resolve(ctx => idPropertyFunc(ctx.Parent<TNode>()));
// This is a bit dodgy but not sure how else to force it.
// Some seem to wanna be String not String!
if (typeof(TId) == typeof(string))
{
dbIdFieldDescriptor.Type<NonNullType<StringType>>();
}
// Call the standard HC setup methods
return descriptor
.ImplementsNode()
.IdField(idProperty)
.ResolveNode(nodeResolver);
}
public static IObjectFieldDescriptor DbIdField<T>(this IObjectTypeDescriptor<T> descriptor)
=> descriptor.Field(FieldNames.DbId);
public static IObjectFieldDescriptor DbIdField(this IObjectTypeDescriptor descriptor)
=> descriptor.Field(FieldNames.DbId);
public static IInterfaceFieldDescriptor DbIdField<T>(this IInterfaceTypeDescriptor<T> descriptor)
=> descriptor.Field(FieldNames.DbId);
public static IInterfaceFieldDescriptor DbIdField(this IInterfaceTypeDescriptor descriptor)
=> descriptor.Field(FieldNames.DbId);
}
}
@williamverdolini
Copy link

For v12 the helpers could be:

#nullable enable

using System.Linq.Expressions;
using HotChocolate.Resolvers;
using HotChocolate.Types.Relay;

namespace HotChocolate.Types
{
    public static class NodeObjectTypeExtensions
    {
        public const string DbId = "dbId";

        /// <summary>
        /// Custom wrapper around built-in Relay node support.
        /// Additionally adds field for `dbId`.
        /// </summary>
        /// <param name="descriptor"></param>
        /// <param name="idProperty">Expression to the property to use for the id.</param>
        /// <param name="dbIdResolver">Resolver for added "dbId" field</param>
        /// <param name="nodeResolver">Node resolver implementation</param>
        /// <typeparam name="TNode"></typeparam>
        /// <typeparam name="TId"></typeparam>
        /// <returns></returns>
        public static IObjectFieldDescriptor AsNodeEx<TNode, TId>(this IObjectTypeDescriptor<TNode> descriptor,
            Expression<Func<TNode, TId>> idProperty,
            Func<IResolverContext, TId> dbIdResolver,
            NodeResolverDelegate<TNode, TId> nodeResolver)
            where TNode : class//?
        {
            // Add dbId which should just return the internal id as is
            var dbIdFieldDescriptor = descriptor.DbIdField().Resolve(dbIdResolver);
            // Call standard stuff
#pragma warning disable CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
            // Suppressed until it's fixed in HC: https://github.com/ChilliCream/hotchocolate/issues/2015
            return descriptor.ImplementsNode().IdField(idProperty).ResolveNode(nodeResolver);
#pragma warning restore CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
        }

        public static IObjectFieldDescriptor DbIdField<T>(this IObjectTypeDescriptor<T> descriptor)
            => descriptor.Field(DbId);

        public static IObjectFieldDescriptor DbIdField(this IObjectTypeDescriptor descriptor)
            => descriptor.Field(DbId);

        public static IInterfaceFieldDescriptor DbIdField<T>(this IInterfaceTypeDescriptor<T> descriptor)
            => descriptor.Field(DbId);

        public static IInterfaceFieldDescriptor DbIdField(this IInterfaceTypeDescriptor descriptor)
            => descriptor.Field(DbId);
    }
}

the usage (in code-first) is like the following:

public class YourEntityType : ObjectType<YourEntity>
{
	protected override void Configure(IObjectTypeDescriptor<YourEntity> descriptor)
	{           
		descriptor.AsNodeEx(
				idProperty: f => f.Id, 
				dbIdResolver: ctx => ctx.Parent<YourEntity>().Id,
				nodeResolver: (ctx, id) =>
					ctx.DataLoader<YourEntityBatchDataLoader>().LoadAsync(id, ctx.RequestAborted));
	}
}

@benmccallum
Copy link
Author

@williamverdolini, we're on v12 now and I found a simpler solution for AsNodeEx that doesn't involve you needing to pass a resolver func for the dbId. Should clean up your code a bit :)

@williamverdolini
Copy link

👍 @benmccallum better

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment