Skip to content

Commit cdbc3e1

Browse files
Adds attribute support for Federation 2 @external and @key directives (#7140)
1 parent 983faa0 commit cdbc3e1

22 files changed

+858
-460
lines changed

src/HotChocolate/ApolloFederation/src/ApolloFederation/Types/Directives/ExternalAttribute.cs

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,30 @@ namespace HotChocolate.ApolloFederation.Types;
2929
/// }
3030
/// </example>
3131
/// </summary>
32-
public sealed class ExternalAttribute : ObjectFieldDescriptorAttribute
32+
[AttributeUsage(
33+
AttributeTargets.Class
34+
| AttributeTargets.Struct
35+
| AttributeTargets.Method
36+
| AttributeTargets.Property)]
37+
public sealed class ExternalAttribute : DescriptorAttribute
3338
{
34-
protected override void OnConfigure(
39+
protected internal override void TryConfigure(
3540
IDescriptorContext context,
36-
IObjectFieldDescriptor descriptor,
37-
MemberInfo member)
38-
=> descriptor.External();
41+
IDescriptor descriptor,
42+
ICustomAttributeProvider element)
43+
{
44+
switch (descriptor)
45+
{
46+
case IObjectTypeDescriptor objectTypeDescriptor:
47+
{
48+
objectTypeDescriptor.External();
49+
break;
50+
}
51+
case IObjectFieldDescriptor objectFieldDescriptor:
52+
{
53+
objectFieldDescriptor.External();
54+
break;
55+
}
56+
}
57+
}
3958
}

src/HotChocolate/ApolloFederation/src/ApolloFederation/Types/Directives/ExternalDescriptorExtensions.cs

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,78 @@ namespace HotChocolate.ApolloFederation.Types;
22

33
public static class ExternalDescriptorExtensions
44
{
5+
/// <summary>
6+
/// Applies the @external directive which indicates that given object and/or field is not usually
7+
/// resolved by the subgraph. If an object is marked as @external then all its fields are
8+
/// automatically external without the need for explicitly marking them with @external directive.
9+
/// All the external fields should either be referenced by the @requires or @provides directives
10+
/// field sets.
11+
///
12+
/// <example>
13+
/// type Foo @key(fields: "id") {
14+
/// id: ID!
15+
/// name: String
16+
/// description: Bar @external # external
17+
/// }
18+
///
19+
/// type Bar @external {
20+
/// description: String # external because Bar is marked external
21+
/// name: String # external because Bar is marked external
22+
/// }
23+
/// </example>
24+
/// </summary>
25+
/// <param name="descriptor">
26+
/// The object type descriptor on which this directive shall be annotated.
27+
/// </param>
28+
/// <returns>
29+
/// Returns the object type descriptor.
30+
/// </returns>
31+
/// <exception cref="ArgumentNullException">
32+
/// <paramref name="descriptor"/> is <c>null</c>.
33+
/// </exception>
34+
public static IObjectTypeDescriptor<T> External<T>(this IObjectTypeDescriptor<T> descriptor)
35+
{
36+
ArgumentNullException.ThrowIfNull(descriptor);
37+
38+
return descriptor.Directive(ExternalDirective.Default);
39+
}
40+
41+
/// <summary>
42+
/// Applies the @external directive which indicates that given object and/or field is not usually
43+
/// resolved by the subgraph. If an object is marked as @external then all its fields are
44+
/// automatically external without the need for explicitly marking them with @external directive.
45+
/// All the external fields should either be referenced by the @requires or @provides directives
46+
/// field sets.
47+
///
48+
/// <example>
49+
/// type Foo @key(fields: "id") {
50+
/// id: ID!
51+
/// name: String
52+
/// description: Bar @external # external
53+
/// }
54+
///
55+
/// type Bar @external {
56+
/// description: String # external because Bar is marked external
57+
/// name: String # external because Bar is marked external
58+
/// }
59+
/// </example>
60+
/// </summary>
61+
/// <param name="descriptor">
62+
/// The object type descriptor on which this directive shall be annotated.
63+
/// </param>
64+
/// <returns>
65+
/// Returns the object type descriptor.
66+
/// </returns>
67+
/// <exception cref="ArgumentNullException">
68+
/// <paramref name="descriptor"/> is <c>null</c>.
69+
/// </exception>
70+
public static IObjectTypeDescriptor External(this IObjectTypeDescriptor descriptor)
71+
{
72+
ArgumentNullException.ThrowIfNull(descriptor);
73+
74+
return descriptor.Directive(ExternalDirective.Default);
75+
}
76+
577
/// <summary>
678
/// Applies the @external directive which is used to mark a field as owned by another service.
779
/// This allows service A to use fields from service B while also knowing at runtime
@@ -36,4 +108,4 @@ public static IObjectFieldDescriptor External(
36108

37109
return descriptor.Directive(ExternalDirective.Default);
38110
}
39-
}
111+
}

src/HotChocolate/ApolloFederation/src/ApolloFederation/Types/Directives/KeyAttribute.cs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ namespace HotChocolate.ApolloFederation.Types;
3737
/// </summary>
3838
[AttributeUsage(
3939
AttributeTargets.Class |
40+
AttributeTargets.Interface |
4041
AttributeTargets.Property,
4142
AllowMultiple = true)]
4243
public sealed class KeyAttribute : DescriptorAttribute
@@ -48,7 +49,7 @@ public KeyAttribute()
4849
{
4950
Resolvable = true;
5051
}
51-
52+
5253
/// <summary>
5354
/// Initializes a new instance of <see cref="KeyAttribute"/>.
5455
/// </summary>
@@ -70,7 +71,7 @@ public KeyAttribute(string fieldSet, bool resolvable = true)
7071
/// Grammatically, a field set is a selection set minus the braces.
7172
/// </summary>
7273
public string? FieldSet { get; }
73-
74+
7475
/// <summary>
7576
/// Gets a value that indicates whether the key is resolvable.
7677
/// </summary>
@@ -90,7 +91,7 @@ protected internal override void TryConfigure(
9091
case PropertyInfo member:
9192
ConfigureField(member, descriptor);
9293
break;
93-
94+
9495
case MethodInfo member:
9596
ConfigureField(member, descriptor);
9697
break;
@@ -109,26 +110,26 @@ private void ConfigureType(Type type, IDescriptor descriptor)
109110
case IObjectTypeDescriptor typeDesc:
110111
typeDesc.Key(FieldSet, Resolvable);
111112
break;
112-
113+
113114
case IInterfaceTypeDescriptor interfaceDesc:
114115
interfaceDesc.Key(FieldSet, Resolvable);
115116
break;
116117
}
117118
}
118-
119+
119120
private void ConfigureField(MemberInfo member, IDescriptor descriptor)
120121
{
121122
if (!string.IsNullOrEmpty(FieldSet))
122123
{
123124
throw Key_FieldSet_MustBeEmpty(member);
124125
}
125-
126+
126127
switch (descriptor)
127128
{
128129
case IObjectFieldDescriptor fieldDesc:
129130
fieldDesc.Extend().Definition.ContextData.TryAdd(KeyMarker, true);
130131
break;
131-
132+
132133
case IInterfaceFieldDescriptor fieldDesc:
133134
fieldDesc.Extend().Definition.ContextData.TryAdd(KeyMarker, true);
134135
break;
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
using System.Threading.Tasks;
2+
using HotChocolate.ApolloFederation.Types;
3+
using HotChocolate.Execution;
4+
using HotChocolate.Types;
5+
using Microsoft.Extensions.DependencyInjection;
6+
using Snapshooter.Xunit;
7+
8+
namespace HotChocolate.ApolloFederation.Directives;
9+
10+
public class ExternalDirectiveTests : FederationTypesTestBase
11+
{
12+
[Fact]
13+
public async Task AnnotateExternalToTypeFieldCodeFirst()
14+
{
15+
// arrange
16+
Snapshot.FullName();
17+
18+
var schema = await new ServiceCollection()
19+
.AddGraphQL()
20+
.AddApolloFederation()
21+
.AddQueryType(o => o
22+
.Name("Query")
23+
.Field("entity")
24+
.Argument("id", a => a.Type<IntType>())
25+
.Type("User")
26+
.Resolve(_ => new { Id = 1 })
27+
)
28+
.AddObjectType(
29+
o =>
30+
{
31+
o.Name("User")
32+
.Key("id");
33+
o.Field("id")
34+
.Type<IntType>()
35+
.Resolve(_ => 1);
36+
o.Field("idCode")
37+
.Type<StringType>()
38+
.Resolve(_ => default!)
39+
.External();
40+
o.Field("address")
41+
.Type("Address")
42+
.Resolve(_ => default!)
43+
.External();
44+
})
45+
.AddObjectType(
46+
o =>
47+
{
48+
o.Name("Address");
49+
o.External();
50+
o.Field("street")
51+
.Type<StringType>()
52+
.Resolve(_ => default!);
53+
o.Field("city")
54+
.Type<StringType>()
55+
.Resolve(_ => default!);
56+
})
57+
.BuildSchemaAsync();
58+
59+
// act
60+
var query = schema.GetType<ObjectType>("User");
61+
var address = schema.GetType<ObjectType>("Address");
62+
63+
// assert
64+
Assert.Collection(
65+
query.Fields["idCode"].Directives,
66+
item => Assert.Equal(FederationTypeNames.ExternalDirective_Name, item.Type.Name));
67+
Assert.Collection(
68+
query.Fields["address"].Directives,
69+
item => Assert.Equal(FederationTypeNames.ExternalDirective_Name, item.Type.Name));
70+
Assert.Collection(
71+
address.Directives,
72+
item => Assert.Equal(FederationTypeNames.ExternalDirective_Name, item.Type.Name));
73+
schema.ToString().MatchSnapshot();
74+
}
75+
76+
[Fact]
77+
public async Task AnnotateExternalToTypeFieldAnnotationBased()
78+
{
79+
// arrange
80+
Snapshot.FullName();
81+
82+
var schema = await new ServiceCollection()
83+
.AddGraphQL()
84+
.AddApolloFederation()
85+
.AddQueryType<Query>()
86+
.BuildSchemaAsync();
87+
88+
// act
89+
var query = schema.GetType<ObjectType>("User");
90+
var address = schema.GetType<ObjectType>("Address");
91+
92+
// assert
93+
Assert.Collection(
94+
query.Fields["idCode"].Directives,
95+
item => Assert.Equal(FederationTypeNames.ExternalDirective_Name, item.Type.Name));
96+
Assert.Collection(
97+
query.Fields["address"].Directives,
98+
item => Assert.Equal(FederationTypeNames.ExternalDirective_Name, item.Type.Name));
99+
Assert.Collection(
100+
address.Directives,
101+
item => Assert.Equal(FederationTypeNames.ExternalDirective_Name, item.Type.Name));
102+
schema.ToString().MatchSnapshot();
103+
}
104+
}
105+
106+
public class Query
107+
{
108+
public User GetEntity(int id) => default!;
109+
}
110+
111+
public class User
112+
{
113+
[Key]
114+
public int Id { get; set; }
115+
[External]
116+
public string IdCode { get; set; } = default!;
117+
[External]
118+
public Address Address { get; set; } = default!;
119+
}
120+
121+
[External]
122+
public class Address
123+
{
124+
public string Street { get; } = default!;
125+
public string City { get; } = default!;
126+
}

0 commit comments

Comments
 (0)