Skip to content

Commit b637886

Browse files
authored
Add support for IDisposable (#24)
* Add support for IDisposable * update workflows * update workflow * Add tests
1 parent 0f48fc7 commit b637886

File tree

11 files changed

+244
-17
lines changed

11 files changed

+244
-17
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
- name: Pack solution [Release]
2828
run: dotnet pack --no-restore --no-build -c Release -p:VersionSuffix=$GITHUB_RUN_NUMBER -o out
2929
- name: Upload artifacts
30-
uses: actions/upload-artifact@v2
30+
uses: actions/upload-artifact@v4
3131
with:
3232
name: Nuget packages
3333
path: |

.github/workflows/publish.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,15 @@ jobs:
3737
- name: Pack solution [Release]
3838
run: dotnet pack --no-restore --no-build -c Release -p:Version=$version -o out
3939
- name: Upload artifacts
40-
uses: actions/upload-artifact@v2
40+
uses: actions/upload-artifact@v4
4141
with:
4242
name: Nuget packages
4343
path: |
4444
out/*
4545
- name: Publish Nuget packages to Nuget registry
4646
run: dotnet nuget push "out/*" -k ${{secrets.NUGET_AUTH_TOKEN}}
4747
- name: Upload nuget packages as release artifacts
48-
uses: actions/github-script@v2
48+
uses: actions/github-script@v7
4949
with:
5050
github-token: ${{secrets.GITHUB_TOKEN}}
5151
script: |

.github/workflows/test.yml

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@ name: Run code tests
22

33
on:
44
pull_request:
5-
branches:
6-
- master
7-
- develop
85
push:
96
branches:
107
- master
@@ -54,9 +51,9 @@ jobs:
5451
reporttypes: 'Html'
5552
tag: 'test_${{ github.run_number }}'
5653
- name: Upload artifacts
57-
uses: actions/upload-artifact@v2
54+
uses: actions/upload-artifact@v4
5855
with:
59-
name: Code coverage artifacts
56+
name: Code coverage artifacts for ${{ matrix.os }}
6057
path: |
6158
${{ matrix.os }}.lcov.info
6259
Clover.xml

Directory.Build.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
<AnalysisMode>Recommended</AnalysisMode>
2525
<GraphQLVersion>8.0.0</GraphQLVersion>
2626
<GraphQLAspNetCore3Version>6.0.0</GraphQLAspNetCore3Version>
27+
<NuGetAuditMode>direct</NuGetAuditMode>
2728
</PropertyGroup>
2829

2930
<ItemGroup>

src/GraphQL.DI/DIObjectGraphBase.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ namespace GraphQL.DI;
99
/// <summary>
1010
/// This is a required base type of all DI-created graph types. <see cref="DIObjectGraphBase"/> may be
1111
/// used if the <see cref="IResolveFieldContext.Source"/> type is <see cref="object"/>.
12+
/// <para>
13+
/// If the derived class implements <see cref="IDisposable"/>, the class must be registered within the DI container.
14+
/// </para>
15+
/// <para>
16+
/// When registered within the DI container, the service lifetime must be <see cref="Microsoft.Extensions.DependencyInjection.ServiceLifetime.Transient">Transient</see>.
17+
/// </para>
1218
/// </summary>
1319
public abstract class DIObjectGraphBase<TSource> : IDIObjectGraphBase<TSource>, IResolveFieldContext<TSource>
1420
{

src/GraphQL.DI/DIObjectGraphType.cs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Diagnostics;
12
using System.Linq.Expressions;
23
using System.Reflection;
34
using GraphQL.Types;
@@ -60,13 +61,36 @@ protected override IEnumerable<MemberInfo> GetRegisteredMembers()
6061
// each field resolver will build a new instance of DIObject
6162
/// <inheritdoc/>
6263
protected override LambdaExpression BuildMemberInstanceExpression(MemberInfo memberInfo)
63-
=> (Expression<Func<IResolveFieldContext, TDIGraph>>)((IResolveFieldContext context) => MemberInstanceFunc(context));
64+
{
65+
// use an explicit type here rather than simply LambdaExpression
66+
Expression<Func<IResolveFieldContext, TDIGraph>> func;
67+
if (typeof(IDisposable).IsAssignableFrom(typeof(TDIGraph))) {
68+
func = (IResolveFieldContext context) => MemberInstanceDisposableFunc(context);
69+
} else {
70+
func = (IResolveFieldContext context) => MemberInstanceFunc(context);
71+
}
72+
return func;
73+
}
6474

6575
/// <inheritdoc/>
6676
private static TDIGraph MemberInstanceFunc(IResolveFieldContext context)
6777
{
6878
// create a new instance of DIObject, filling in any constructor arguments from DI
69-
var graph = ActivatorUtilities.GetServiceOrCreateInstance<TDIGraph>(context.RequestServices ?? throw new MissingRequestServicesException());
79+
var graph = ActivatorUtilities.GetServiceOrCreateInstance<TDIGraph>(context.RequestServices ?? ThrowMissingRequestServicesException());
80+
// set the context
81+
graph.Context = context;
82+
// return the object
83+
return graph;
84+
85+
static IServiceProvider ThrowMissingRequestServicesException() => throw new MissingRequestServicesException();
86+
}
87+
88+
/// <inheritdoc/>
89+
private static TDIGraph MemberInstanceDisposableFunc(IResolveFieldContext context)
90+
{
91+
// pull DIObject from dependency injection, as it is disposable and must be managed by DI
92+
var graph = (context.RequestServices ?? throw new MissingRequestServicesException()).GetService<TDIGraph>()
93+
?? throw new InvalidOperationException($"Could not resolve an instance of {typeof(TDIGraph).Name} from the service provider. DI graph types that implement IDisposable must be registered in the service provider.");
7094
// set the context
7195
graph.Context = context;
7296
// return the object

src/GraphQL.DI/GraphQLBuilderExtensions.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,26 @@ public static IGraphQLBuilder AddDIGraphTypes(this IGraphQLBuilder builder)
1818
return builder;
1919
}
2020

21+
/// <summary>
22+
/// Scans the calling assembly for classes that implement <see cref="IDIObjectGraphBase{TSource}"/>
23+
/// and registers them as transients within the DI container.
24+
/// </summary>
25+
public static IGraphQLBuilder AddDIGraphBases(this IGraphQLBuilder builder)
26+
=> AddDIGraphBases(builder, Assembly.GetCallingAssembly());
27+
28+
/// <summary>
29+
/// Scans the specified assembly for classes that implement <see cref="IDIObjectGraphBase"/>
30+
/// and registers them as transients within the DI container.
31+
/// </summary>
32+
public static IGraphQLBuilder AddDIGraphBases(this IGraphQLBuilder builder, Assembly assembly)
33+
{
34+
foreach (var type in assembly.GetTypes()
35+
.Where(x => x.IsClass && !x.IsAbstract && typeof(IDIObjectGraphBase).IsAssignableFrom(x))) {
36+
builder.Services.TryRegister(type, type, ServiceLifetime.Transient);
37+
}
38+
return builder;
39+
}
40+
2141
/// <summary>
2242
/// Scans the calling assembly for classes that implement <see cref="IDIObjectGraphBase{TSource}"/> and
2343
/// registers clr type mappings on the schema between that <see cref="DIObjectGraphType{TDIGraph, TSource}"/>
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
using System.Reflection;
2+
using GraphQL.DI;
3+
4+
namespace GraphQL;
5+
6+
/// <summary>
7+
/// Provides extension methods to configure GraphQL.NET services within a dependency injection framework.
8+
/// </summary>
9+
public static class GraphQLDIBuilderExtensions
10+
{
11+
/// <summary>
12+
/// Performs the following:
13+
/// <list type="bullet">
14+
/// <item>
15+
/// Registers <see cref="DIObjectGraphType{TDIGraph}"/> and
16+
/// <see cref="DIObjectGraphType{TDIGraph, TSource}"/> as generic types.
17+
/// </item>
18+
/// <item>
19+
/// Scans the calling assembly for classes that implement <see cref="IDIObjectGraphBase{TSource}"/>
20+
/// and registers them as transients within the DI container.
21+
/// </item>
22+
/// <item>
23+
/// Scans the calling assembly for classes that implement <see cref="IDIObjectGraphBase{TSource}"/> and
24+
/// registers clr type mappings on the schema between that <see cref="DIObjectGraphType{TDIGraph, TSource}"/>
25+
/// (constructed from that class and its source type), and the source type.
26+
/// Skips classes where the source type is <see cref="object"/>, or where the class is marked with
27+
/// the <see cref="DoNotMapClrTypeAttribute"/>, or where another graph type would be automatically mapped
28+
/// to the specified type, or where a graph type has already been registered to the specified clr type.
29+
/// </item>
30+
/// </list>
31+
/// </summary>
32+
public static IGraphQLBuilder AddDI(this IGraphQLBuilder builder)
33+
=> AddDI(builder, Assembly.GetCallingAssembly());
34+
35+
/// <summary>
36+
/// Performs the following:
37+
/// <list type="bullet">
38+
/// <item>
39+
/// Registers <see cref="DIObjectGraphType{TDIGraph}"/> and
40+
/// <see cref="DIObjectGraphType{TDIGraph, TSource}"/> as generic types.
41+
/// </item>
42+
/// <item>
43+
/// Scans the specified assembly for classes that implement <see cref="IDIObjectGraphBase{TSource}"/>
44+
/// and registers them as transients within the DI container.
45+
/// </item>
46+
/// <item>
47+
/// Scans the specified assembly for classes that implement <see cref="IDIObjectGraphBase{TSource}"/> and
48+
/// registers clr type mappings on the schema between that <see cref="DIObjectGraphType{TDIGraph, TSource}"/>
49+
/// (constructed from that class and its source type), and the source type.
50+
/// Skips classes where the source type is <see cref="object"/>, or where the class is marked with
51+
/// the <see cref="DoNotMapClrTypeAttribute"/>, or where another graph type would be automatically mapped
52+
/// to the specified type, or where a graph type has already been registered to the specified clr type.
53+
/// </item>
54+
/// </list>
55+
/// </summary>
56+
public static IGraphQLBuilder AddDI(this IGraphQLBuilder builder, Assembly assembly)
57+
{
58+
return builder
59+
.AddDIGraphTypes()
60+
.AddDIGraphBases(assembly)
61+
.AddDIClrTypeMappings(assembly);
62+
}
63+
}

src/Tests/DIObjectGraphTypeTests/DIObjectGraphTypeTestBase.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,17 @@ public DIObjectGraphTypeTestBase() : base()
3131
_contextMock.SetupGet(x => x.Schema).Returns((ISchema)null!);
3232
}
3333

34-
protected IComplexGraphType Configure<T, TSource>(bool instance = false, bool scoped = false) where T : DIObjectGraphBase<TSource>, new()
34+
protected IComplexGraphType Configure<T, TSource>(bool instance = false, bool scoped = false, bool registered = true) where T : DIObjectGraphBase<TSource>, new()
3535
{
3636
if (instance) {
37-
if (scoped) {
38-
_scopedServiceProviderMock.Setup(x => x.GetService(typeof(T))).Returns(() => new T()).Verifiable();
37+
if (registered) {
38+
if (scoped) {
39+
_scopedServiceProviderMock.Setup(x => x.GetService(typeof(T))).Returns(() => new T()).Verifiable();
40+
} else {
41+
_serviceProviderMock.Setup(x => x.GetService(typeof(T))).Returns(() => new T()).Verifiable();
42+
}
3943
} else {
40-
_serviceProviderMock.Setup(x => x.GetService(typeof(T))).Returns(() => new T()).Verifiable();
44+
_serviceProviderMock.Setup(x => x.GetService(typeof(T))).Returns(() => null).Verifiable();
4145
}
4246
}
4347
_graphType = new DIObjectGraphType<T, TSource>();

src/Tests/DIObjectGraphTypeTests/Field.cs

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@ public class CStaticMethod : DIObjectGraphBase
1919
public static string? Field1() => "hello";
2020
}
2121

22-
[Fact]
23-
public void InstanceMethod()
22+
[Theory]
23+
[InlineData(true)]
24+
[InlineData(false)]
25+
public void InstanceMethod(bool registered)
2426
{
25-
Configure<CInstanceMethod, object>(true);
27+
Configure<CInstanceMethod, object>(true, registered: registered);
2628
VerifyField("Field1", nullable: true, concurrent: false, returnValue: "hello");
2729
Verify(false);
2830
}
@@ -443,6 +445,31 @@ public class CIgnore : DIObjectGraphBase<object>
443445
public static string Field2() => "hello";
444446
}
445447

448+
[Fact]
449+
public void DisposableRegistered()
450+
{
451+
Configure<CDisposable, object>(true);
452+
VerifyField("FieldTest", true, false, "hello");
453+
Verify(false);
454+
}
455+
456+
[Fact]
457+
public async Task DisposableUnRegistered()
458+
{
459+
Configure<CDisposable, object>(true, registered: false);
460+
var err = await Should.ThrowAsync<InvalidOperationException>(() => VerifyFieldAsync("FieldTest", true, false, "hello"));
461+
err.Message.ShouldBe("Could not resolve an instance of CDisposable from the service provider. DI graph types that implement IDisposable must be registered in the service provider.");
462+
Verify(false);
463+
}
464+
465+
public class CDisposable : DIObjectGraphBase, IDisposable
466+
{
467+
[Name("FieldTest")]
468+
public string? Field1() => "hello";
469+
470+
void IDisposable.Dispose() => GC.SuppressFinalize(this);
471+
}
472+
446473
[Fact]
447474
public void AddsMetadata()
448475
{

0 commit comments

Comments
 (0)